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