init
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# OpenAI API Configuration
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# Other API Keys (optional)
|
||||||
|
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
# GROQ_API_KEY=your_groq_api_key_here
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Python 虚拟环境
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Python 缓存文件
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# IDE 配置(可选)
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# 项目临时文件
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -1,2 +1,174 @@
|
|||||||
# more_dots
|
# LangChain + LangGraph Scaffolding
|
||||||
|
|
||||||
|
一个使用 LangChain 和 LangGraph 构建的 AI 应用脚手架项目,提供模块化的代理和工作流管理。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🚀 **模块化架构**: 基于代理和工作流的模块化设计
|
||||||
|
- 🔧 **工具集成**: 支持自定义工具和函数调用
|
||||||
|
- 💬 **多轮对话**: 内置对话状态管理和上下文维护
|
||||||
|
- 📊 **工作流管理**: 多种工作流类型,支持会话和工具使用
|
||||||
|
- ⚙️ **配置管理**: 统一的环境变量和配置管理
|
||||||
|
- 🧪 **测试支持**: 包含基础测试和示例代码
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
more_dots/
|
||||||
|
├── agents/ # 代理模块
|
||||||
|
│ ├── base_agent.py # 基础代理类
|
||||||
|
│ ├── conversation_agent.py # 对话代理
|
||||||
|
│ └── tool_agent.py # 工具使用代理
|
||||||
|
├── tools/ # 工具模块
|
||||||
|
│ ├── calculator.py # 计算器工具
|
||||||
|
│ └── web_search.py # 网络搜索工具(占位符)
|
||||||
|
├── workflows/ # 工作流管理
|
||||||
|
│ └── workflow_manager.py # 工作流管理器
|
||||||
|
├── examples/ # 使用示例
|
||||||
|
│ └── basic_usage.py # 基础用法示例
|
||||||
|
├── tests/ # 测试文件
|
||||||
|
│ └── test_basic.py # 基础测试
|
||||||
|
├── config.py # 配置文件
|
||||||
|
├── requirements.txt # 依赖包列表
|
||||||
|
├── .env.example # 环境变量示例
|
||||||
|
├── main.py # 主程序入口
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制环境变量文件
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 编辑 .env 文件,设置你的 OpenAI API 密钥
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行基础示例
|
||||||
|
python examples/basic_usage.py
|
||||||
|
|
||||||
|
# 运行交互式 CLI
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```python
|
||||||
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
|
# 创建工作流管理器
|
||||||
|
manager = WorkflowManager()
|
||||||
|
|
||||||
|
# 使用对话工作流
|
||||||
|
result = manager.execute_workflow(
|
||||||
|
WorkflowType.CONVERSATION,
|
||||||
|
"Hello! How can you help me?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 使用工具工作流
|
||||||
|
result = manager.execute_workflow(
|
||||||
|
WorkflowType.TOOL_USING,
|
||||||
|
"Calculate 15 * 3 + 7"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义工具
|
||||||
|
|
||||||
|
创建新的工具类:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
class CustomTool(BaseTool):
|
||||||
|
name = "custom_tool"
|
||||||
|
description = "A custom tool for specific tasks"
|
||||||
|
|
||||||
|
def _run(self, input: str) -> str:
|
||||||
|
# 实现工具逻辑
|
||||||
|
return f"Processed: {input}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展代理
|
||||||
|
|
||||||
|
创建新的代理类型:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agents.base_agent import BaseAgent
|
||||||
|
|
||||||
|
class CustomAgent(BaseAgent):
|
||||||
|
def _build_graph(self):
|
||||||
|
# 实现自定义图结构
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _custom_node(self, state):
|
||||||
|
# 自定义节点逻辑
|
||||||
|
return state
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工作流类型
|
||||||
|
|
||||||
|
| 工作流类型 | 描述 | 适用场景 |
|
||||||
|
|-----------|------|----------|
|
||||||
|
| `conversation` | 多轮对话代理 | 聊天机器人、客服系统 |
|
||||||
|
| `tool_using` | 工具使用代理 | 任务执行、数据分析 |
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
|
||||||
|
1. **新工具**: 在 `tools/` 目录下创建新的工具类
|
||||||
|
2. **新代理**: 在 `agents/` 目录下继承 `BaseAgent` 类
|
||||||
|
3. **新工作流**: 在 `workflows/` 目录下扩展工作流管理器
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
python -m pytest tests/
|
||||||
|
|
||||||
|
# 运行特定测试
|
||||||
|
python -m pytest tests/test_basic.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试
|
||||||
|
|
||||||
|
项目使用标准的 Python 日志系统,可以通过设置环境变量启用调试模式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖项
|
||||||
|
|
||||||
|
主要依赖包:
|
||||||
|
|
||||||
|
- `langchain-core`: LangChain 核心功能
|
||||||
|
- `langchain`: LangChain 主包
|
||||||
|
- `langgraph`: LangGraph 图工作流
|
||||||
|
- `langchain-openai`: OpenAI 集成
|
||||||
|
- `python-dotenv`: 环境变量管理
|
||||||
|
- `pydantic`: 数据验证
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request 来改进这个项目!
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState:
|
||||||
|
"""State definition for the agent workflow"""
|
||||||
|
messages: List[BaseMessage]
|
||||||
|
current_step: str
|
||||||
|
context: Dict[str, Any]
|
||||||
|
|
||||||
|
def __init__(self, messages: List[BaseMessage] = None, current_step: str = "start", context: Dict[str, Any] = None):
|
||||||
|
self.messages = messages or []
|
||||||
|
self.current_step = current_step
|
||||||
|
self.context = context or {}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgent:
|
||||||
|
"""Base agent class with common functionality"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = Config.DEFAULT_MODEL):
|
||||||
|
self.model = ChatOpenAI(
|
||||||
|
model=model_name,
|
||||||
|
api_key=Config.OPENAI_API_KEY,
|
||||||
|
temperature=0.1,
|
||||||
|
max_retries=Config.MAX_RETRIES,
|
||||||
|
timeout=Config.TIMEOUT
|
||||||
|
)
|
||||||
|
self.graph = self._build_graph()
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""Build the state graph for the agent"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Add nodes and edges
|
||||||
|
workflow.add_node("process_input", self._process_input)
|
||||||
|
workflow.add_node("generate_response", self._generate_response)
|
||||||
|
|
||||||
|
# Define edges
|
||||||
|
workflow.add_edge("process_input", "generate_response")
|
||||||
|
workflow.add_edge("generate_response", END)
|
||||||
|
|
||||||
|
# Set entry point
|
||||||
|
workflow.set_entry_point("process_input")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _process_input(self, state: AgentState) -> AgentState:
|
||||||
|
"""Process user input"""
|
||||||
|
# This is a base implementation - subclasses should override
|
||||||
|
state.current_step = "processed"
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _generate_response(self, state: AgentState) -> AgentState:
|
||||||
|
"""Generate response using the LLM"""
|
||||||
|
if state.messages:
|
||||||
|
response = self.model.invoke(state.messages)
|
||||||
|
state.messages.append(response)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""Run the agent with user input"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.messages,
|
||||||
|
"context": result.context,
|
||||||
|
"final_step": result.current_step
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from typing import Dict, Any, List
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from .base_agent import BaseAgent, AgentState
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationAgent(BaseAgent):
|
||||||
|
"""Agent for handling multi-turn conversations"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = None):
|
||||||
|
super().__init__(model_name)
|
||||||
|
self.conversation_history: List[BaseMessage] = []
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""Build conversation-specific graph"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
|
workflow.add_node("analyze_intent", self._analyze_intent)
|
||||||
|
workflow.add_node("generate_response", self._generate_response)
|
||||||
|
workflow.add_node("update_context", self._update_context)
|
||||||
|
|
||||||
|
# Define edges
|
||||||
|
workflow.add_edge("analyze_intent", "generate_response")
|
||||||
|
workflow.add_edge("generate_response", "update_context")
|
||||||
|
workflow.add_edge("update_context", END)
|
||||||
|
|
||||||
|
# Set entry point
|
||||||
|
workflow.set_entry_point("analyze_intent")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _analyze_intent(self, state: AgentState) -> AgentState:
|
||||||
|
"""Analyze user intent and conversation context"""
|
||||||
|
# Simple intent analysis - can be enhanced with more sophisticated logic
|
||||||
|
user_message = state.messages[-1] if state.messages else None
|
||||||
|
|
||||||
|
if user_message and isinstance(user_message, HumanMessage):
|
||||||
|
content = user_message.content.lower()
|
||||||
|
|
||||||
|
# Basic intent detection
|
||||||
|
if any(word in content for word in ["hello", "hi", "hey", "greetings"]):
|
||||||
|
state.context["intent"] = "greeting"
|
||||||
|
elif any(word in content for word in ["help", "assist", "support"]):
|
||||||
|
state.context["intent"] = "help"
|
||||||
|
elif "?" in content:
|
||||||
|
state.context["intent"] = "question"
|
||||||
|
else:
|
||||||
|
state.context["intent"] = "general"
|
||||||
|
|
||||||
|
state.current_step = "intent_analyzed"
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _generate_response(self, state: AgentState) -> AgentState:
|
||||||
|
"""Generate response considering conversation history"""
|
||||||
|
# Combine conversation history with current message
|
||||||
|
all_messages = self.conversation_history + state.messages
|
||||||
|
|
||||||
|
if all_messages:
|
||||||
|
response = self.model.invoke(all_messages)
|
||||||
|
state.messages.append(response)
|
||||||
|
|
||||||
|
state.current_step = "response_generated"
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _update_context(self, state: AgentState) -> AgentState:
|
||||||
|
"""Update conversation context and history"""
|
||||||
|
# Add the conversation to history (excluding system messages)
|
||||||
|
for message in state.messages:
|
||||||
|
if isinstance(message, (HumanMessage, AIMessage)):
|
||||||
|
self.conversation_history.append(message)
|
||||||
|
|
||||||
|
# Limit conversation history to avoid token limits
|
||||||
|
if len(self.conversation_history) > 10:
|
||||||
|
self.conversation_history = self.conversation_history[-10:]
|
||||||
|
|
||||||
|
state.current_step = "context_updated"
|
||||||
|
return state
|
||||||
|
|
||||||
|
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""Run conversation with history management"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.messages,
|
||||||
|
"context": result.context,
|
||||||
|
"conversation_history": self.conversation_history,
|
||||||
|
"final_step": result.current_step
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from langgraph.prebuilt import ToolNode
|
||||||
|
from .base_agent import BaseAgent, AgentState
|
||||||
|
from tools.calculator import CalculatorTool
|
||||||
|
from tools.web_search import WebSearchTool
|
||||||
|
|
||||||
|
|
||||||
|
class ToolAgent(BaseAgent):
|
||||||
|
"""Agent that can use tools to accomplish tasks"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = None, tools: List[BaseTool] = None):
|
||||||
|
# Initialize with default tools if none provided
|
||||||
|
if tools is None:
|
||||||
|
tools = [CalculatorTool(), WebSearchTool()]
|
||||||
|
|
||||||
|
self.tools = tools
|
||||||
|
self.tool_node = ToolNode(tools)
|
||||||
|
super().__init__(model_name)
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""Build tool-using graph"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
|
workflow.add_node("agent", self._agent_node)
|
||||||
|
workflow.add_node("tools", self.tool_node)
|
||||||
|
|
||||||
|
# Define edges
|
||||||
|
workflow.add_edge("tools", "agent")
|
||||||
|
|
||||||
|
# Conditional routing
|
||||||
|
workflow.add_conditional_edges(
|
||||||
|
"agent",
|
||||||
|
self._should_use_tools,
|
||||||
|
{
|
||||||
|
"tools": "tools",
|
||||||
|
"end": END,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set entry point
|
||||||
|
workflow.set_entry_point("agent")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _agent_node(self, state: AgentState) -> AgentState:
|
||||||
|
"""Agent node that decides whether to use tools"""
|
||||||
|
# Bind tools to the model
|
||||||
|
model_with_tools = self.model.bind_tools(self.tools)
|
||||||
|
|
||||||
|
# Get the last message
|
||||||
|
if state.messages:
|
||||||
|
response = model_with_tools.invoke(state.messages)
|
||||||
|
state.messages.append(response)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _should_use_tools(self, state: AgentState) -> str:
|
||||||
|
"""Determine if tools should be used"""
|
||||||
|
last_message = state.messages[-1]
|
||||||
|
|
||||||
|
# If the last message has tool calls, route to tools
|
||||||
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
|
return "tools"
|
||||||
|
|
||||||
|
# Otherwise, end the workflow
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""Run the tool-using agent"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.messages,
|
||||||
|
"context": result.context,
|
||||||
|
"tools_used": [tool.name for tool in self.tools],
|
||||||
|
"final_step": result.current_step
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Application configuration"""
|
||||||
|
|
||||||
|
# API Keys
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
|
||||||
|
# Model configurations
|
||||||
|
DEFAULT_MODEL = "gpt-4o"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
TIMEOUT = 30
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_config(cls):
|
||||||
|
"""Validate that required configuration is present"""
|
||||||
|
if not cls.OPENAI_API_KEY:
|
||||||
|
raise ValueError("OPENAI_API_KEY is required. Please set it in your .env file")
|
||||||
|
|
||||||
|
# Validate configuration on import
|
||||||
|
Config.validate_config()
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Basic usage examples for the LangChain + LangGraph scaffolding
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
|
|
||||||
|
def example_conversation():
|
||||||
|
"""Example of using the conversation workflow"""
|
||||||
|
print("=== Conversation Workflow Example ===")
|
||||||
|
|
||||||
|
manager = WorkflowManager()
|
||||||
|
|
||||||
|
# First message
|
||||||
|
result1 = manager.execute_workflow(
|
||||||
|
WorkflowType.CONVERSATION,
|
||||||
|
"Hello! Can you help me with some calculations?"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Session ID: {result1['session_id']}")
|
||||||
|
print(f"Response: {result1['result']['messages'][-1].content}")
|
||||||
|
|
||||||
|
# Second message in the same session
|
||||||
|
result2 = manager.execute_workflow(
|
||||||
|
WorkflowType.CONVERSATION,
|
||||||
|
"What can you help me with?",
|
||||||
|
session_id=result1['session_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Second response: {result2['result']['messages'][-1].content}")
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def example_tool_usage():
|
||||||
|
"""Example of using the tool workflow"""
|
||||||
|
print("=== Tool Workflow Example ===")
|
||||||
|
|
||||||
|
manager = WorkflowManager()
|
||||||
|
|
||||||
|
# Use calculator tool
|
||||||
|
result = manager.execute_workflow(
|
||||||
|
WorkflowType.TOOL_USING,
|
||||||
|
"Calculate 25 * 4 + 10"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Session ID: {result['session_id']}")
|
||||||
|
|
||||||
|
# Extract tool messages and responses
|
||||||
|
for message in result['result']['messages']:
|
||||||
|
if hasattr(message, 'tool_calls') and message.tool_calls:
|
||||||
|
print(f"Tool call: {message.tool_calls}")
|
||||||
|
elif hasattr(message, 'content'):
|
||||||
|
print(f"Response: {message.content}")
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def list_available_workflows():
|
||||||
|
"""List all available workflows"""
|
||||||
|
print("=== Available Workflows ===")
|
||||||
|
|
||||||
|
manager = WorkflowManager()
|
||||||
|
workflows = manager.get_available_workflows()
|
||||||
|
|
||||||
|
for workflow in workflows:
|
||||||
|
print(f"- {workflow}")
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Check if configuration is valid
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
Config.validate_config()
|
||||||
|
print("✅ Configuration is valid")
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
# Run examples
|
||||||
|
list_available_workflows()
|
||||||
|
example_conversation()
|
||||||
|
example_tool_usage()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Configuration error: {e}")
|
||||||
|
print("\nPlease make sure to:")
|
||||||
|
print("1. Copy .env.example to .env")
|
||||||
|
print("2. Set your OPENAI_API_KEY in the .env file")
|
||||||
|
print("3. Install dependencies: pip install -r requirements.txt")
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Main entry point for the LangChain + LangGraph scaffolding project
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_cli():
|
||||||
|
"""Interactive command-line interface"""
|
||||||
|
print("🚀 LangChain + LangGraph Scaffolding")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
manager = WorkflowManager()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\nAvailable workflows:")
|
||||||
|
for i, workflow in enumerate(manager.get_available_workflows(), 1):
|
||||||
|
print(f"{i}. {workflow}")
|
||||||
|
print("0. Exit")
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice = input("\nSelect workflow (0-2): ").strip()
|
||||||
|
|
||||||
|
if choice == "0":
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
elif choice == "1":
|
||||||
|
workflow_type = WorkflowType.CONVERSATION
|
||||||
|
print("\n💬 Conversation Mode - Type 'quit' to return to menu")
|
||||||
|
elif choice == "2":
|
||||||
|
workflow_type = WorkflowType.TOOL_USING
|
||||||
|
print("\n🔧 Tool Mode - Type 'quit' to return to menu")
|
||||||
|
else:
|
||||||
|
print("Invalid choice")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Interactive session
|
||||||
|
session_id = None
|
||||||
|
while True:
|
||||||
|
user_input = input("\nYou: ").strip()
|
||||||
|
|
||||||
|
if user_input.lower() in ['quit', 'exit', 'q']:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = manager.execute_workflow(
|
||||||
|
workflow_type,
|
||||||
|
user_input,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
session_id = result['session_id']
|
||||||
|
|
||||||
|
# Extract and display the response
|
||||||
|
last_message = result['result']['messages'][-1]
|
||||||
|
if hasattr(last_message, 'content'):
|
||||||
|
print(f"AI: {last_message.content}")
|
||||||
|
|
||||||
|
# Show tool usage if any
|
||||||
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
|
print(f"🔧 Tools used: {[tc['name'] for tc in last_message.tool_calls]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nGoodbye!")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
Config.validate_config()
|
||||||
|
interactive_cli()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Configuration error: {e}")
|
||||||
|
print("\nPlease make sure to:")
|
||||||
|
print("1. Copy .env.example to .env")
|
||||||
|
print("2. Set your OPENAI_API_KEY in the .env file")
|
||||||
|
print("3. Install dependencies: pip install -r requirements.txt")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
langchain-core>=0.3.0
|
||||||
|
langchain>=0.3.0
|
||||||
|
langgraph>=0.2.0
|
||||||
|
langchain-openai>=0.2.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
pydantic>=2.0.0
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Basic tests for the LangChain + LangGraph scaffolding
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkflowManager(unittest.TestCase):
|
||||||
|
"""Test WorkflowManager functionality"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures"""
|
||||||
|
self.manager = WorkflowManager()
|
||||||
|
|
||||||
|
def test_get_available_workflows(self):
|
||||||
|
"""Test that available workflows are returned"""
|
||||||
|
workflows = self.manager.get_available_workflows()
|
||||||
|
self.assertIsInstance(workflows, list)
|
||||||
|
self.assertGreater(len(workflows), 0)
|
||||||
|
self.assertIn("conversation", workflows)
|
||||||
|
self.assertIn("tool_using", workflows)
|
||||||
|
|
||||||
|
def test_get_workflow(self):
|
||||||
|
"""Test getting workflow instances"""
|
||||||
|
conversation_workflow = self.manager.get_workflow(WorkflowType.CONVERSATION)
|
||||||
|
self.assertIsNotNone(conversation_workflow)
|
||||||
|
|
||||||
|
tool_workflow = self.manager.get_workflow(WorkflowType.TOOL_USING)
|
||||||
|
self.assertIsNotNone(tool_workflow)
|
||||||
|
|
||||||
|
def test_session_management(self):
|
||||||
|
"""Test session creation and retrieval"""
|
||||||
|
# Execute a workflow to create a session
|
||||||
|
result = self.manager.execute_workflow(
|
||||||
|
WorkflowType.CONVERSATION,
|
||||||
|
"Hello, test session"
|
||||||
|
)
|
||||||
|
|
||||||
|
session_id = result["session_id"]
|
||||||
|
self.assertIsNotNone(session_id)
|
||||||
|
|
||||||
|
# Test session info retrieval
|
||||||
|
session_info = self.manager.get_session_info(session_id)
|
||||||
|
self.assertIsNotNone(session_info)
|
||||||
|
self.assertEqual(session_info["workflow_type"], WorkflowType.CONVERSATION)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfiguration(unittest.TestCase):
|
||||||
|
"""Test configuration validation"""
|
||||||
|
|
||||||
|
def test_config_import(self):
|
||||||
|
"""Test that configuration can be imported"""
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
# This should not raise an exception if .env file exists with valid API key
|
||||||
|
self.assertTrue(hasattr(Config, 'OPENAI_API_KEY'))
|
||||||
|
except ImportError:
|
||||||
|
self.fail("Could not import config module")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import Dict, Any
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatorTool(BaseTool):
|
||||||
|
"""A simple calculator tool for mathematical operations"""
|
||||||
|
|
||||||
|
name: str = "calculator"
|
||||||
|
description: str = "Perform mathematical calculations. Input should be a mathematical expression like '2 + 2' or '10 * (3 + 5)'"
|
||||||
|
|
||||||
|
def _run(self, expression: str) -> str:
|
||||||
|
"""Evaluate a mathematical expression"""
|
||||||
|
try:
|
||||||
|
# Security: Only allow safe mathematical operations
|
||||||
|
allowed_chars = set("0123456789+-*/(). ")
|
||||||
|
if not all(c in allowed_chars for c in expression):
|
||||||
|
return "Error: Expression contains invalid characters"
|
||||||
|
|
||||||
|
# Evaluate the expression
|
||||||
|
result = eval(expression)
|
||||||
|
return f"Result: {result}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error calculating expression: {str(e)}"
|
||||||
|
|
||||||
|
async def _arun(self, expression: str) -> str:
|
||||||
|
"""Async version of the tool"""
|
||||||
|
return self._run(expression)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Dict, Any, List
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class WebSearchTool(BaseTool):
|
||||||
|
"""A tool for searching the web (placeholder implementation)"""
|
||||||
|
|
||||||
|
name: str = "web_search"
|
||||||
|
description: str = "Search the web for information. Input should be a search query."
|
||||||
|
|
||||||
|
def _run(self, query: str) -> str:
|
||||||
|
"""Search the web for information"""
|
||||||
|
# This is a placeholder implementation
|
||||||
|
# In a real implementation, you would integrate with a search API
|
||||||
|
# like Serper, Tavily, or Google Search API
|
||||||
|
|
||||||
|
return f"Web search functionality for query: '{query}' is not implemented. This is a placeholder. To implement real web search, you would need to:
|
||||||
|
1. Sign up for a search API service (e.g., Serper, Tavily)
|
||||||
|
2. Add your API key to the .env file
|
||||||
|
3. Implement the actual search logic here"
|
||||||
|
|
||||||
|
async def _arun(self, query: str) -> str:
|
||||||
|
"""Async version of the tool"""
|
||||||
|
return self._run(query)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from enum import Enum
|
||||||
|
from agents.conversation_agent import ConversationAgent
|
||||||
|
from agents.tool_agent import ToolAgent
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowType(Enum):
|
||||||
|
"""Available workflow types"""
|
||||||
|
CONVERSATION = "conversation"
|
||||||
|
TOOL_USING = "tool_using"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowManager:
|
||||||
|
"""Manages different workflow types and their execution"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.workflows = {
|
||||||
|
WorkflowType.CONVERSATION: ConversationAgent(),
|
||||||
|
WorkflowType.TOOL_USING: ToolAgent()
|
||||||
|
}
|
||||||
|
self.active_sessions: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def get_workflow(self, workflow_type: WorkflowType):
|
||||||
|
"""Get a workflow instance"""
|
||||||
|
return self.workflows.get(workflow_type)
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_type: WorkflowType, user_input: str,
|
||||||
|
session_id: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""Execute a specific workflow"""
|
||||||
|
workflow = self.get_workflow(workflow_type)
|
||||||
|
|
||||||
|
if not workflow:
|
||||||
|
return {"error": f"Workflow {workflow_type.value} not found"}
|
||||||
|
|
||||||
|
# Generate session ID if not provided
|
||||||
|
if not session_id:
|
||||||
|
session_id = f"session_{len(self.active_sessions) + 1}"
|
||||||
|
|
||||||
|
# Execute the workflow
|
||||||
|
result = workflow.run(user_input, **kwargs)
|
||||||
|
|
||||||
|
# Store session data
|
||||||
|
self.active_sessions[session_id] = {
|
||||||
|
"workflow_type": workflow_type,
|
||||||
|
"last_result": result,
|
||||||
|
"timestamp": self._get_timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"session_id": session_id,
|
||||||
|
"workflow_type": workflow_type.value,
|
||||||
|
"result": result
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_available_workflows(self) -> List[str]:
|
||||||
|
"""Get list of available workflow types"""
|
||||||
|
return [workflow.value for workflow in WorkflowType]
|
||||||
|
|
||||||
|
def _get_timestamp(self) -> str:
|
||||||
|
"""Get current timestamp"""
|
||||||
|
from datetime import datetime
|
||||||
|
return datetime.now().isoformat()
|
||||||
|
|
||||||
|
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get information about a session"""
|
||||||
|
return self.active_sessions.get(session_id)
|
||||||
|
|
||||||
|
def cleanup_sessions(self, older_than_hours: int = 24):
|
||||||
|
"""Clean up old sessions"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
|
||||||
|
|
||||||
|
sessions_to_remove = []
|
||||||
|
for session_id, session_data in self.active_sessions.items():
|
||||||
|
session_time = datetime.fromisoformat(session_data["timestamp"])
|
||||||
|
if session_time < cutoff_time:
|
||||||
|
sessions_to_remove.append(session_id)
|
||||||
|
|
||||||
|
for session_id in sessions_to_remove:
|
||||||
|
del self.active_sessions[session_id]
|
||||||
|
|
||||||
|
return len(sessions_to_remove)
|
||||||
Reference in New Issue
Block a user