This commit is contained in:
cjw
2026-02-17 02:31:39 +08:00
parent 2f0e25c400
commit 27a1b8a7e9
14 changed files with 877 additions and 1 deletions
+6
View File
@@ -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
View File
@@ -0,0 +1,17 @@
# Python 虚拟环境
venv/
.venv/
# Python 缓存文件
__pycache__/
*.pyc
*.pyo
*.pyd
# IDE 配置(可选)
.idea/
.vscode/
# 项目临时文件
.DS_Store
*.log
+173 -1
View File
@@ -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 来改进这个项目!
+76
View File
@@ -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
}
+94
View File
@@ -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
}
+86
View File
@@ -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
}
+27
View File
@@ -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()
+94
View File
@@ -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")
+95
View File
@@ -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()
+6
View File
@@ -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
+68
View File
@@ -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()
+27
View File
@@ -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)
+25
View File
@@ -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)
+83
View File
@@ -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)