init
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
# 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
|
|
||||||
+4
-1
@@ -14,4 +14,7 @@ __pycache__/
|
|||||||
|
|
||||||
# 项目临时文件
|
# 项目临时文件
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# 本地配置
|
||||||
|
config/config.ini
|
||||||
|
|||||||
@@ -9,30 +9,51 @@
|
|||||||
- 💬 **多轮对话**: 内置对话状态管理和上下文维护
|
- 💬 **多轮对话**: 内置对话状态管理和上下文维护
|
||||||
- 📊 **工作流管理**: 多种工作流类型,支持会话和工具使用
|
- 📊 **工作流管理**: 多种工作流类型,支持会话和工具使用
|
||||||
- ⚙️ **配置管理**: 统一的环境变量和配置管理
|
- ⚙️ **配置管理**: 统一的环境变量和配置管理
|
||||||
|
- 🧩 **FastAPI 接入**: 提供 HTTP 接口对外服务
|
||||||
|
- 🧭 **Nacos 注册**: 支持服务注册与心跳
|
||||||
- 🧪 **测试支持**: 包含基础测试和示例代码
|
- 🧪 **测试支持**: 包含基础测试和示例代码
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
more_dots/
|
more_dots/
|
||||||
├── agents/ # 代理模块
|
├── agent/ # Agent 核心逻辑层
|
||||||
│ ├── base_agent.py # 基础代理类
|
│ ├── graph.py # LangGraph 图结构定义
|
||||||
│ ├── conversation_agent.py # 对话代理
|
│ ├── nodes.py # 节点执行逻辑
|
||||||
│ └── tool_agent.py # 工具使用代理
|
│ ├── state.py # Agent 状态定义
|
||||||
├── tools/ # 工具模块
|
│ ├── conversation.py # 对话代理
|
||||||
│ ├── calculator.py # 计算器工具
|
│ └── tool.py # 工具代理
|
||||||
│ └── web_search.py # 网络搜索工具(占位符)
|
├── api/ # API 接口层
|
||||||
├── workflows/ # 工作流管理
|
│ ├── endpoints.py # FastAPI 路由定义
|
||||||
│ └── workflow_manager.py # 工作流管理器
|
│ └── dependencies.py # API 依赖注入
|
||||||
├── examples/ # 使用示例
|
├── services/ # 服务层
|
||||||
│ └── basic_usage.py # 基础用法示例
|
│ ├── llm_factory.py # LLM 实例工厂
|
||||||
├── tests/ # 测试文件
|
│ └── nacos_service.py # Nacos 集成
|
||||||
│ └── test_basic.py # 基础测试
|
├── schemas/ # 数据模型层
|
||||||
├── config.py # 配置文件
|
│ ├── agent_input.py # 输入模型
|
||||||
|
│ └── agent_output.py # 输出模型
|
||||||
|
├── config/ # 配置层
|
||||||
|
│ └── settings.py # 配置读取
|
||||||
|
│ └── prompts.yaml # 提示词配置
|
||||||
|
│ └── ragflow_templates/ # RAGFlow 模板(表名 -> 模板列表)
|
||||||
|
│ └── table_metadata_prompts/ # 表模型元数据提示词
|
||||||
|
├── tools/ # 工具模块
|
||||||
|
│ ├── calculator.py # 计算器工具
|
||||||
|
│ └── web_search.py # 网络搜索工具(占位符)
|
||||||
|
├── workflows/ # 工作流管理
|
||||||
|
│ └── workflow_manager.py
|
||||||
|
├── examples/ # 使用示例
|
||||||
|
│ └── basic_usage.py
|
||||||
|
├── tests/ # 测试文件
|
||||||
|
│ └── test_basic.py
|
||||||
├── requirements.txt # 依赖包列表
|
├── requirements.txt # 依赖包列表
|
||||||
├── .env.example # 环境变量示例
|
├── config/
|
||||||
├── main.py # 主程序入口
|
│ ├── config.ini.example # 配置文件示例
|
||||||
└── README.md # 项目说明
|
│ ├── config.ini # 本地配置(需自行创建)
|
||||||
|
│ └── prompts.yaml # 提示词配置
|
||||||
|
├── server.py # FastAPI 服务入口
|
||||||
|
├── main.py # CLI 入口
|
||||||
|
└── README.md # 项目说明
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
@@ -43,14 +64,25 @@ more_dots/
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 配置环境变量
|
### 2. 配置 config.ini
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 复制环境变量文件
|
# 复制配置文件
|
||||||
cp .env.example .env
|
cp config/config.ini.example config/config.ini
|
||||||
|
|
||||||
# 编辑 .env 文件,设置你的 OpenAI API 密钥
|
# 编辑 config/config.ini,设置你的 API Key,并可添加多个模型配置
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
[General]
|
||||||
|
DEFAULT_MODEL_SECTION = gpt-4o
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
TIMEOUT = 30
|
||||||
|
|
||||||
|
[gpt-4o]
|
||||||
|
MODEL_NAME = gpt-4o
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
|
|
||||||
|
[gpt-3.5-turbo]
|
||||||
|
MODEL_NAME = gpt-3.5-turbo
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 运行示例
|
### 3. 运行示例
|
||||||
@@ -59,8 +91,14 @@ OPENAI_API_KEY=your_openai_api_key_here
|
|||||||
# 运行基础示例
|
# 运行基础示例
|
||||||
python examples/basic_usage.py
|
python examples/basic_usage.py
|
||||||
|
|
||||||
# 运行交互式 CLI
|
# 运行交互式 CLI(默认模型)
|
||||||
python main.py
|
python main.py
|
||||||
|
|
||||||
|
# 运行交互式 CLI(指定模型配置段)
|
||||||
|
python main.py gpt-3.5-turbo
|
||||||
|
|
||||||
|
# 运行 FastAPI 服务
|
||||||
|
python server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用指南
|
## 使用指南
|
||||||
@@ -70,9 +108,55 @@ python main.py
|
|||||||
```python
|
```python
|
||||||
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
# 创建工作流管理器
|
# 创建工作流管理器(默认模型)
|
||||||
manager = WorkflowManager()
|
manager = WorkflowManager()
|
||||||
|
|
||||||
|
# 创建工作流管理器(指定模型配置段)
|
||||||
|
manager_alt = WorkflowManager(default_model_section="gpt-3.5-turbo")
|
||||||
|
|
||||||
|
### FastAPI 接口
|
||||||
|
|
||||||
|
启动服务后,可使用以下接口:
|
||||||
|
|
||||||
|
- `GET /health`:健康检查
|
||||||
|
- `GET /nacos/status`:查看 Nacos 注册状态
|
||||||
|
- `POST /api/workflows`:执行工作流
|
||||||
|
|
||||||
|
示例请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input": "你好,帮我算 1 + 2",
|
||||||
|
"session_id": null,
|
||||||
|
"workflow_type": "conversation"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nacos 配置
|
||||||
|
|
||||||
|
在 `config/config.ini` 中开启 Nacos:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[nacos]
|
||||||
|
enabled = true
|
||||||
|
server = localhost:8848
|
||||||
|
namespace = public
|
||||||
|
group_name = DEFAULT_GROUP
|
||||||
|
cluster_name = DEFAULT
|
||||||
|
heartbeat_interval = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### RAGFlow 模板同步
|
||||||
|
|
||||||
|
模板文件位于 `config/ragflow_templates`,每个 JSON 对应一个表名与模板列表。
|
||||||
|
同步脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/sync_ragflow_templates.py
|
||||||
|
```
|
||||||
|
|
||||||
|
请在 `config/config.ini` 中配置 `ragflow.upload` 上传接口。
|
||||||
|
|
||||||
# 使用对话工作流
|
# 使用对话工作流
|
||||||
result = manager.execute_workflow(
|
result = manager.execute_workflow(
|
||||||
WorkflowType.CONVERSATION,
|
WorkflowType.CONVERSATION,
|
||||||
@@ -161,7 +245,6 @@ logging.basicConfig(level=logging.DEBUG)
|
|||||||
- `langchain`: LangChain 主包
|
- `langchain`: LangChain 主包
|
||||||
- `langgraph`: LangGraph 图工作流
|
- `langgraph`: LangGraph 图工作流
|
||||||
- `langchain-openai`: OpenAI 集成
|
- `langchain-openai`: OpenAI 集成
|
||||||
- `python-dotenv`: 环境变量管理
|
|
||||||
- `pydantic`: 数据验证
|
- `pydantic`: 数据验证
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .state import AgentState
|
||||||
|
from .graph import BaseAgent
|
||||||
|
from .conversation import ConversationAgent
|
||||||
|
from .tool import ToolAgent
|
||||||
|
|
||||||
|
__all__ = ["AgentState", "BaseAgent", "ConversationAgent", "ToolAgent"]
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
|
||||||
|
from .graph import BaseAgent
|
||||||
|
from .state import AgentState
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationAgent(BaseAgent):
|
||||||
|
"""处理多轮对话的代理"""
|
||||||
|
|
||||||
|
def __init__(self, model_section: Optional[str] = None):
|
||||||
|
super().__init__(model_section)
|
||||||
|
self.conversation_history: List[BaseMessage] = []
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""构建对话专用图"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
workflow.add_node("analyze_intent", self._analyze_intent)
|
||||||
|
workflow.add_node("normalize_input", self._normalize_input)
|
||||||
|
workflow.add_node("generate_response", self._generate_response)
|
||||||
|
workflow.add_node("update_context", self._update_context)
|
||||||
|
|
||||||
|
workflow.add_edge("analyze_intent", "normalize_input")
|
||||||
|
workflow.add_edge("normalize_input", "generate_response")
|
||||||
|
workflow.add_edge("generate_response", "update_context")
|
||||||
|
workflow.add_edge("update_context", END)
|
||||||
|
|
||||||
|
workflow.set_entry_point("analyze_intent")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _analyze_intent(self, state: AgentState) -> AgentState:
|
||||||
|
"""分析用户意图与对话上下文"""
|
||||||
|
user_message = state.messages[-1] if state.messages else None
|
||||||
|
|
||||||
|
if user_message and isinstance(user_message, HumanMessage):
|
||||||
|
content = user_message.content.lower()
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""结合对话历史生成回复"""
|
||||||
|
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:
|
||||||
|
"""更新对话上下文与历史"""
|
||||||
|
for message in state.messages:
|
||||||
|
if isinstance(message, (HumanMessage, AIMessage)):
|
||||||
|
self.conversation_history.append(message)
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""运行对话并维护历史"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.get("messages", []),
|
||||||
|
"context": result.get("context", {}),
|
||||||
|
"conversation_history": self.conversation_history,
|
||||||
|
"final_step": result.get("current_step", "unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
def stream_run(self, user_input: str):
|
||||||
|
"""流式运行对话并维护历史"""
|
||||||
|
all_messages = self.conversation_history + [HumanMessage(content=user_input)]
|
||||||
|
full_text = ""
|
||||||
|
|
||||||
|
for chunk in self.model.stream(all_messages):
|
||||||
|
if hasattr(chunk, "content") and chunk.content:
|
||||||
|
full_text += chunk.content
|
||||||
|
yield chunk.content
|
||||||
|
|
||||||
|
self.conversation_history.append(HumanMessage(content=user_input))
|
||||||
|
self.conversation_history.append(AIMessage(content=full_text))
|
||||||
|
|
||||||
|
if len(self.conversation_history) > 10:
|
||||||
|
self.conversation_history = self.conversation_history[-10:]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
|
||||||
|
from services.llm_factory import create_chat_model
|
||||||
|
from .state import AgentState
|
||||||
|
from . import nodes
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgent:
|
||||||
|
"""包含通用功能的基础代理类"""
|
||||||
|
|
||||||
|
def __init__(self, model_section: Optional[str] = None):
|
||||||
|
self.model = create_chat_model(model_section)
|
||||||
|
self.graph = self._build_graph()
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""构建代理状态图"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
workflow.add_node("process_input", nodes.process_input)
|
||||||
|
workflow.add_node("normalize_input", self._normalize_input)
|
||||||
|
workflow.add_node("generate_response", self._generate_response)
|
||||||
|
|
||||||
|
workflow.add_edge("process_input", "normalize_input")
|
||||||
|
workflow.add_edge("normalize_input", "generate_response")
|
||||||
|
workflow.add_edge("generate_response", END)
|
||||||
|
|
||||||
|
workflow.set_entry_point("process_input")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _generate_response(self, state: AgentState) -> AgentState:
|
||||||
|
"""使用 LLM 生成回复"""
|
||||||
|
return nodes.generate_response(state, self.model)
|
||||||
|
|
||||||
|
def _normalize_input(self, state: AgentState) -> AgentState:
|
||||||
|
"""规范化用户输入"""
|
||||||
|
return nodes.normalize_input(state, self.model)
|
||||||
|
|
||||||
|
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""运行代理并处理用户输入"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.get("messages", []),
|
||||||
|
"context": result.get("context", {}),
|
||||||
|
"final_step": result.get("current_step", "unknown")
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
||||||
|
from .state import AgentState
|
||||||
|
from services.prompt_manager import PromptManager
|
||||||
|
from services.template_matcher import TemplateMatcher
|
||||||
|
|
||||||
|
|
||||||
|
def process_input(state: AgentState) -> AgentState:
|
||||||
|
"""处理用户输入"""
|
||||||
|
state.current_step = "processed"
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def generate_response(state: AgentState, model) -> AgentState:
|
||||||
|
"""使用 LLM 生成回复"""
|
||||||
|
if state.messages:
|
||||||
|
response = model.invoke(state.messages)
|
||||||
|
state.messages.append(response)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_input(state: AgentState, model) -> AgentState:
|
||||||
|
"""将用户输入规范化为标准英文语句"""
|
||||||
|
if not state.messages:
|
||||||
|
return state
|
||||||
|
|
||||||
|
last_message = state.messages[-1]
|
||||||
|
if not isinstance(last_message, HumanMessage):
|
||||||
|
return state
|
||||||
|
|
||||||
|
prompt_manager = PromptManager()
|
||||||
|
system_prompt = SystemMessage(
|
||||||
|
content=prompt_manager.get("system", "english_normalizer")
|
||||||
|
)
|
||||||
|
|
||||||
|
response = model.invoke([system_prompt, HumanMessage(content=last_message.content)])
|
||||||
|
normalized = response.content if hasattr(response, "content") else str(response)
|
||||||
|
|
||||||
|
state.context["original_input"] = last_message.content
|
||||||
|
state.context["normalized_input"] = normalized
|
||||||
|
|
||||||
|
matcher = TemplateMatcher()
|
||||||
|
state.context["table_match"] = matcher.match(normalized)
|
||||||
|
return state
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from typing import Any, Dict, List
|
||||||
|
from langchain_core.messages import BaseMessage
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState:
|
||||||
|
"""代理工作流的状态定义"""
|
||||||
|
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 {}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
|
from .graph import BaseAgent
|
||||||
|
from .state import AgentState
|
||||||
|
from tools.calculator import CalculatorTool
|
||||||
|
from tools.web_search import WebSearchTool
|
||||||
|
from tools.rest_api_tool import RestApiTool
|
||||||
|
from tools.sr_api_tool import SrApiQueryTool
|
||||||
|
|
||||||
|
|
||||||
|
class ToolAgent(BaseAgent):
|
||||||
|
"""可使用工具完成任务的代理"""
|
||||||
|
|
||||||
|
def __init__(self, model_section: Optional[str] = None, tools: List[BaseTool] = None):
|
||||||
|
if tools is None:
|
||||||
|
tools = [CalculatorTool(), WebSearchTool(), RestApiTool(), SrApiQueryTool()]
|
||||||
|
|
||||||
|
self.tools = tools
|
||||||
|
self.tool_node = ToolNode(tools)
|
||||||
|
super().__init__(model_section)
|
||||||
|
|
||||||
|
def _build_graph(self) -> StateGraph:
|
||||||
|
"""构建可使用工具的图"""
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
workflow.add_node("normalize_input", self._normalize_input)
|
||||||
|
workflow.add_node("agent", self._agent_node)
|
||||||
|
workflow.add_node("tools", self.tool_node)
|
||||||
|
|
||||||
|
workflow.add_edge("normalize_input", "agent")
|
||||||
|
workflow.add_edge("tools", "agent")
|
||||||
|
|
||||||
|
workflow.add_conditional_edges(
|
||||||
|
"agent",
|
||||||
|
self._should_use_tools,
|
||||||
|
{
|
||||||
|
"tools": "tools",
|
||||||
|
"end": END,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.set_entry_point("normalize_input")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
def _agent_node(self, state: AgentState) -> AgentState:
|
||||||
|
"""决定是否调用工具的代理节点"""
|
||||||
|
model_with_tools = self.model.bind_tools(self.tools)
|
||||||
|
|
||||||
|
if state.messages:
|
||||||
|
try:
|
||||||
|
response = model_with_tools.invoke(state.messages)
|
||||||
|
except Exception as e:
|
||||||
|
error_text = str(e)
|
||||||
|
if "tool choice" in error_text and "auto" in error_text:
|
||||||
|
fallback_model = self.model.bind_tools(self.tools, tool_choice="none")
|
||||||
|
response = fallback_model.invoke(state.messages)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
state.messages.append(response)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _should_use_tools(self, state: AgentState) -> str:
|
||||||
|
"""判断是否需要使用工具"""
|
||||||
|
last_message = state.messages[-1]
|
||||||
|
|
||||||
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
|
return "tools"
|
||||||
|
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""运行工具型代理"""
|
||||||
|
initial_state = AgentState(
|
||||||
|
messages=[HumanMessage(content=user_input)],
|
||||||
|
context=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.graph.invoke(initial_state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": result.get("messages", []),
|
||||||
|
"context": result.get("context", {}),
|
||||||
|
"tools_used": [tool.name for tool in self.tools],
|
||||||
|
"final_step": result.get("current_step", "unknown")
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Agent 内部工具函数(按需扩展)
|
||||||
+4
-74
@@ -1,76 +1,6 @@
|
|||||||
from typing import Any, Dict, List, Optional
|
"""兼容导出:请优先使用 agent 包"""
|
||||||
from langchain_core.messages import BaseMessage, HumanMessage
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langgraph.graph import StateGraph, END
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
|
from agent.graph import BaseAgent
|
||||||
|
from agent.state import AgentState
|
||||||
|
|
||||||
class AgentState:
|
__all__ = ["BaseAgent", "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
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
||||||
from langgraph.graph import StateGraph, END
|
from langgraph.graph import StateGraph, END
|
||||||
from .base_agent import BaseAgent, AgentState
|
from .base_agent import BaseAgent, AgentState
|
||||||
|
|
||||||
|
|
||||||
class ConversationAgent(BaseAgent):
|
class ConversationAgent(BaseAgent):
|
||||||
"""Agent for handling multi-turn conversations"""
|
"""处理多轮对话的代理"""
|
||||||
|
|
||||||
def __init__(self, model_name: str = None):
|
def __init__(self, model_section: Optional[str] = None):
|
||||||
super().__init__(model_name)
|
super().__init__(model_section)
|
||||||
self.conversation_history: List[BaseMessage] = []
|
self.conversation_history: List[BaseMessage] = []
|
||||||
|
|
||||||
def _build_graph(self) -> StateGraph:
|
def _build_graph(self) -> StateGraph:
|
||||||
"""Build conversation-specific graph"""
|
"""构建对话专用图"""
|
||||||
workflow = StateGraph(AgentState)
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
# Add nodes
|
# 添加节点
|
||||||
workflow.add_node("analyze_intent", self._analyze_intent)
|
workflow.add_node("analyze_intent", self._analyze_intent)
|
||||||
workflow.add_node("generate_response", self._generate_response)
|
workflow.add_node("generate_response", self._generate_response)
|
||||||
workflow.add_node("update_context", self._update_context)
|
workflow.add_node("update_context", self._update_context)
|
||||||
|
|
||||||
# Define edges
|
# 定义边
|
||||||
workflow.add_edge("analyze_intent", "generate_response")
|
workflow.add_edge("analyze_intent", "generate_response")
|
||||||
workflow.add_edge("generate_response", "update_context")
|
workflow.add_edge("generate_response", "update_context")
|
||||||
workflow.add_edge("update_context", END)
|
workflow.add_edge("update_context", END)
|
||||||
|
|
||||||
# Set entry point
|
# 设置入口节点
|
||||||
workflow.set_entry_point("analyze_intent")
|
workflow.set_entry_point("analyze_intent")
|
||||||
|
|
||||||
return workflow.compile()
|
return workflow.compile()
|
||||||
|
|
||||||
def _analyze_intent(self, state: AgentState) -> AgentState:
|
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
|
user_message = state.messages[-1] if state.messages else None
|
||||||
|
|
||||||
if user_message and isinstance(user_message, HumanMessage):
|
if user_message and isinstance(user_message, HumanMessage):
|
||||||
content = user_message.content.lower()
|
content = user_message.content.lower()
|
||||||
|
|
||||||
# Basic intent detection
|
# 基础意图识别
|
||||||
if any(word in content for word in ["hello", "hi", "hey", "greetings"]):
|
if any(word in content for word in ["hello", "hi", "hey", "greetings"]):
|
||||||
state.context["intent"] = "greeting"
|
state.context["intent"] = "greeting"
|
||||||
elif any(word in content for word in ["help", "assist", "support"]):
|
elif any(word in content for word in ["help", "assist", "support"]):
|
||||||
@@ -46,49 +46,8 @@ class ConversationAgent(BaseAgent):
|
|||||||
elif "?" in content:
|
elif "?" in content:
|
||||||
state.context["intent"] = "question"
|
state.context["intent"] = "question"
|
||||||
else:
|
else:
|
||||||
state.context["intent"] = "general"
|
"""兼容导出:请优先使用 agent 包"""
|
||||||
|
|
||||||
state.current_step = "intent_analyzed"
|
from agent.conversation import ConversationAgent
|
||||||
return state
|
|
||||||
|
__all__ = ["ConversationAgent"]
|
||||||
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
|
|
||||||
}
|
|
||||||
+3
-84
@@ -1,86 +1,5 @@
|
|||||||
from typing import Dict, Any, List, Optional
|
"""兼容导出:请优先使用 agent 包"""
|
||||||
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
|
|
||||||
|
|
||||||
|
from agent.tool import ToolAgent
|
||||||
|
|
||||||
class ToolAgent(BaseAgent):
|
__all__ = ["ToolAgent"]
|
||||||
"""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 @@
|
|||||||
|
"""API 包"""
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
|
||||||
|
def get_workflow_manager(request: Request):
|
||||||
|
return request.app.state.workflow_manager
|
||||||
|
|
||||||
|
|
||||||
|
def get_nacos_manager(request: Request):
|
||||||
|
return request.app.state.nacos_manager
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_config(request: Request):
|
||||||
|
return request.app.state.service_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_router(request: Request):
|
||||||
|
return request.app.state.tool_router
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from schemas.agent_input import AgentInput
|
||||||
|
from schemas.agent_output import AgentOutput
|
||||||
|
from schemas.tool_input import ToolInput
|
||||||
|
from schemas.tool_output import ToolOutput
|
||||||
|
from workflows.workflow_manager import WorkflowType
|
||||||
|
from api.dependencies import get_workflow_manager, get_nacos_manager, get_service_config, get_tool_router
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_workflow_type(value: str) -> WorkflowType:
|
||||||
|
try:
|
||||||
|
return WorkflowType(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"不支持的工作流类型: {value}") from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
def health_check(service_config=Depends(get_service_config)):
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"service_name": service_config.service_name,
|
||||||
|
"model_section": service_config.metadata.get("model_section", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/nacos/status")
|
||||||
|
def nacos_status(nacos_manager=Depends(get_nacos_manager)):
|
||||||
|
return nacos_manager.status()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/workflows", response_model=AgentOutput)
|
||||||
|
def run_workflow(payload: AgentInput, workflow_manager=Depends(get_workflow_manager)):
|
||||||
|
try:
|
||||||
|
workflow_type = _resolve_workflow_type(payload.workflow_type)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
result = workflow_manager.execute_workflow(
|
||||||
|
workflow_type=workflow_type,
|
||||||
|
user_input=payload.input,
|
||||||
|
session_id=payload.session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return AgentOutput(
|
||||||
|
session_id=result["session_id"],
|
||||||
|
workflow_type=result["workflow_type"],
|
||||||
|
result=result["result"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/workflows/stream")
|
||||||
|
def run_workflow_stream(payload: AgentInput, workflow_manager=Depends(get_workflow_manager)):
|
||||||
|
try:
|
||||||
|
workflow_type = _resolve_workflow_type(payload.workflow_type)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
if workflow_type != WorkflowType.CONVERSATION:
|
||||||
|
raise HTTPException(status_code=400, detail="仅支持对话工作流的流式输出")
|
||||||
|
|
||||||
|
agent = workflow_manager.get_workflow(workflow_type)
|
||||||
|
|
||||||
|
def event_stream():
|
||||||
|
try:
|
||||||
|
for token in agent.stream_run(payload.input):
|
||||||
|
yield f"data: {token}\n\n"
|
||||||
|
yield "event: end\ndata: [DONE]\n\n"
|
||||||
|
except Exception as e:
|
||||||
|
yield f"event: error\ndata: {str(e)}\n\n"
|
||||||
|
|
||||||
|
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/tools/execute", response_model=ToolOutput)
|
||||||
|
def run_tool(payload: ToolInput, tool_router=Depends(get_tool_router)):
|
||||||
|
result = tool_router.call(payload.tool_name, payload.payload)
|
||||||
|
return ToolOutput(**result)
|
||||||
@@ -1,27 +1,5 @@
|
|||||||
import os
|
"""兼容导出:请优先使用 config 包"""
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Load environment variables
|
from config.settings import Config
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
class Config:
|
__all__ = ["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,3 @@
|
|||||||
|
from .settings import Config
|
||||||
|
|
||||||
|
__all__ = ["Config"]
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
[General]
|
||||||
|
# 默认使用的模型配置区域
|
||||||
|
DEFAULT_MODEL_SECTION = gpt-4o
|
||||||
|
# 最大重试次数
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
# 请求超时时间
|
||||||
|
TIMEOUT = 30
|
||||||
|
|
||||||
|
[gpt-4o]
|
||||||
|
# 模型名称
|
||||||
|
MODEL_NAME = gpt-4o
|
||||||
|
# API 密钥
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
|
|
||||||
|
[gpt-3.5-turbo]
|
||||||
|
# 模型名称
|
||||||
|
MODEL_NAME = gpt-3.5-turbo
|
||||||
|
# API 密钥
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
|
|
||||||
|
[app]
|
||||||
|
# 服务名称
|
||||||
|
service_name = more-dots-api
|
||||||
|
# 监听地址
|
||||||
|
host = 0.0.0.0
|
||||||
|
# 监听端口
|
||||||
|
port = 8000
|
||||||
|
# 服务版本
|
||||||
|
version = 1.0.0
|
||||||
|
# 默认模型配置段(可覆盖 General.DEFAULT_MODEL_SECTION)
|
||||||
|
model_section = gpt-4o
|
||||||
|
|
||||||
|
[ragflow]
|
||||||
|
url = http://10.122.176.97:21020
|
||||||
|
dataset_ids = f5b8b854d63a11f083230242c0a8e006
|
||||||
|
document_ids = 819cf100f52611f0a7fa0242c0a8e006
|
||||||
|
api_key = ragflow-xxxxx
|
||||||
|
retrieval = /api/v1/retrieval
|
||||||
|
upload = /api/v1/documents
|
||||||
|
cache_ttl = 600
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
enabled = false
|
||||||
|
url = redis://localhost:6379/0
|
||||||
|
db = 0
|
||||||
|
|
||||||
|
[nacos]
|
||||||
|
# 是否启用 Nacos 注册
|
||||||
|
enabled = false
|
||||||
|
# Nacos 服务器地址
|
||||||
|
server = localhost:8848
|
||||||
|
# 命名空间
|
||||||
|
namespace = public
|
||||||
|
# 分组名称
|
||||||
|
group_name = DEFAULT_GROUP
|
||||||
|
# 集群名称
|
||||||
|
cluster_name = DEFAULT
|
||||||
|
# 账号(可选)
|
||||||
|
username =
|
||||||
|
# 密码(可选)
|
||||||
|
password =
|
||||||
|
# 心跳间隔(秒)
|
||||||
|
heartbeat_interval = 5
|
||||||
|
# 权重
|
||||||
|
weight = 1.0
|
||||||
|
# 是否临时实例
|
||||||
|
ephemeral = true
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
system:
|
||||||
|
default: |
|
||||||
|
You are a helpful assistant. Follow the user's instructions carefully.
|
||||||
|
sql_generator: |
|
||||||
|
You are an assistant that converts user intent into SQL.
|
||||||
|
Ensure the SQL is correct, safe, and syntactically valid.
|
||||||
|
|
||||||
|
user:
|
||||||
|
default: |
|
||||||
|
Please respond clearly and concisely.
|
||||||
|
english_normalizer: |
|
||||||
|
You are a translation and normalization assistant.
|
||||||
|
Convert the user's input to a clear, grammatically correct English sentence suitable for SQL intent.
|
||||||
|
If the input is already English, polish it.
|
||||||
|
Return only the final English sentence without extra explanations.
|
||||||
|
|
||||||
|
business:
|
||||||
|
sql_generation: |
|
||||||
|
Based on the user's intent, generate a SQL query.
|
||||||
|
Use correct table/field names and return only SQL.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
本目录存放用于 RAGFlow 匹配的模板配置(JSON)。
|
||||||
|
|
||||||
|
约定:每个 JSON 文件对应一个表名(key),包含模板列表(value)。
|
||||||
|
示例文件:orders.json、customers.json。
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"table": "example_table",
|
||||||
|
"templates": [
|
||||||
|
"example_table created in {date}",
|
||||||
|
"count of records in example_table",
|
||||||
|
"example_table where status = {status}"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""从 config.ini 读取的应用配置"""
|
||||||
|
|
||||||
|
_config = configparser.ConfigParser()
|
||||||
|
_root_dir = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
_config_path = os.path.join(_root_dir, 'config', 'config.ini')
|
||||||
|
|
||||||
|
# 在类初始化时加载配置
|
||||||
|
if not os.path.exists(_config_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Configuration file not found at: {_config_path}. "
|
||||||
|
"Please copy 'config/config.ini.example' to 'config/config.ini' and fill in your details."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(_config_path, "r", encoding="utf-8") as f:
|
||||||
|
_config.read_file(f)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
with open(_config_path, "r", encoding="gbk") as f:
|
||||||
|
_config.read_file(f)
|
||||||
|
|
||||||
|
# 通用设置
|
||||||
|
DEFAULT_MODEL_SECTION: str = _config.get('General', 'DEFAULT_MODEL_SECTION', fallback='gpt-4o')
|
||||||
|
MAX_RETRIES: int = _config.getint('General', 'MAX_RETRIES', fallback=3)
|
||||||
|
TIMEOUT: int = _config.getint('General', 'TIMEOUT', fallback=30)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_model_config(cls, section: Optional[str] = None) -> dict:
|
||||||
|
"""
|
||||||
|
获取指定模型配置段。
|
||||||
|
若 section 为 None,则使用默认模型配置段。
|
||||||
|
"""
|
||||||
|
if section is None:
|
||||||
|
section = cls.DEFAULT_MODEL_SECTION
|
||||||
|
|
||||||
|
if not cls._config.has_section(section):
|
||||||
|
raise ValueError(f"Model section '{section}' not found in config.ini")
|
||||||
|
|
||||||
|
config = dict(cls._config.items(section))
|
||||||
|
|
||||||
|
# 确保必需的键存在
|
||||||
|
if 'model_name' not in config or 'openai_api_key' not in config:
|
||||||
|
raise ValueError(f"Model section '{section}' must contain 'model_name' and 'openai_api_key'")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": config['model_name'],
|
||||||
|
"api_key": config['openai_api_key'],
|
||||||
|
"base_url": config.get('url') or config.get('base_url')
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_section(cls, section: str) -> dict:
|
||||||
|
"""获取指定配置段的键值对(键名会被转为小写)"""
|
||||||
|
if not cls._config.has_section(section):
|
||||||
|
return {}
|
||||||
|
return dict(cls._config.items(section))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_config(cls):
|
||||||
|
"""校验默认模型配置是否存在且有效"""
|
||||||
|
try:
|
||||||
|
default_config = cls.get_model_config()
|
||||||
|
if not default_config.get("api_key") or 'your_openai_api_key_here' in default_config.get("api_key"):
|
||||||
|
raise ValueError(f"API key for default model '{cls.DEFAULT_MODEL_SECTION}' is missing or a placeholder.")
|
||||||
|
except (ValueError, configparser.Error) as e:
|
||||||
|
raise ValueError(f"Configuration validation failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# 如有需要可在导入时做初始校验,
|
||||||
|
# 但已移到 main.py 以便更可控地执行。
|
||||||
|
# 如需在导入时校验,可在此调用 Config.validate_config()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
本目录存放业务 SQL 提示词(JSON 格式)。
|
||||||
|
|
||||||
|
约定:每个 JSON 文件对应一个数据库表模型。
|
||||||
|
示例文件:order.json、customer.json 等。
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"domain": "订单物料最新状态与历史变更查询",
|
||||||
|
"description": "此模型用于查询订单物料的最新状态和ETA相关信息,支持查询历史变更记录。该表包含最新状态数据(data_flag='Newest')和历史变更数据(data_flag='Changelog')的联合结果。所有日期字段在查询时自动转换为字符类型。通过data_flag区分最新状态和历史变更,实现订单全生命周期追踪。",
|
||||||
|
"data_source": "dwd_ai.apbo_eta_ful",
|
||||||
|
"system_flag_limit": "任何条件下,都不允许在SELECT子句中查询is_passdue, is_dummy, data_flag字段,仅用于WHERE过滤,该规则优先级最高!"
|
||||||
|
},
|
||||||
|
"data_model_specification": {
|
||||||
|
"fields_list": [
|
||||||
|
"service_order_id", "soid", "part_number", "topmost_pn", "commodity_code", "ship_to_country",
|
||||||
|
"region", "dc_plant", "mtm", "machine_sn", "machine_type", "whether_premier", "stm_planner",
|
||||||
|
"category", "lenovo_ref_no", "case_number", "service_order_creation_date", "eta", "so_eta",
|
||||||
|
"status", "status_date", "update_date", "parts_sales", "warranty", "aging_day", "action",
|
||||||
|
"is_passdue", "is_dummy", "data_flag", "model", "aging_range", "recovery_day", "recovery_range",
|
||||||
|
"order_type", "key_lenovo_ref_no", "service_type", "customer", "life_cycle", "hawb", "bol", "dn", "po", "prid"
|
||||||
|
],
|
||||||
|
"mandatory_display_fields": {
|
||||||
|
"rule1": "默认展示字段(按以下顺序):service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date",
|
||||||
|
"rule2": "用户明确提到的字段优先原则:如果用户问题中明确提到了某些字段,这些字段应在SELECT子句中排在最前面,按用户问题中出现的顺序排列",
|
||||||
|
"rule3": "字段去重:用户提到的字段和默认字段有重叠时,每个字段只出现一次",
|
||||||
|
"rule4": "日期字段必须转换为字符类型:cast(column as varchar(4096)) as column",
|
||||||
|
"rule5": "where子句中出现的字段,必须加入select中查询展示",
|
||||||
|
"rule6": "禁止使用limit",
|
||||||
|
"rule7": "请确认生成的sql中,用户使用的是mt(machine_type)还是mtm, 注意!! MTM SN = machine_sn, MT SN = machine_sn, mt = machine_type",
|
||||||
|
"rule8": "当用户要求:展示所有信息,所有字段,全字段等需求时候,额外加入展示的字段:machine_type,case_number,status_date,aging_day,is_passdue,is_dummy,data_flag,recovery_day,recovery_range,hawb,bol,dn,po,prid",
|
||||||
|
"rule9": "where条件禁止使用status字段过滤",
|
||||||
|
"rule10": "用户输入了汇总,统计,分组等关键词,但未明确统计方式时候,默认使用count(*)进行汇总,注意group by必须和聚合函数成对出现,聚合查询后默认按照聚合的值从小到大排序",
|
||||||
|
"rule11": "如果用户输入的关键字有:VN,AU,IN,HK,PH,KR,ID,TW,MO,FJ,LK,MY,SG,NZ,TH,JP,BD,则视为国家字段的数值过滤条件,增加ship_to_country = '国家代码'",
|
||||||
|
"rule12": "MM/DD类似格式日期转换为 like '%-MM-DD%' ,DD等格式转换为 like '%-DD%'",
|
||||||
|
"rule13": "用户指定日期时,去掉data_flag = 'Newest'的过滤条件,如:查询2026年1月26的eta信息,则where子句中不包含data_flag = 'Newest',而是update_date = '2025-12-31'",
|
||||||
|
"rule14": "聚合查询中,如未使用group by进行分组,select中默认增加使用update_date字段"
|
||||||
|
},
|
||||||
|
"optional_fields": {
|
||||||
|
"key_identifier_fields": ["service_order_id", "soid", "part_number","topmost_pn", "commodity_code"],
|
||||||
|
"milestone_date_fields": ["eta", "so_eta", "status_date", "update_date", "service_order_creation_date"],
|
||||||
|
"location_fields": ["ship_to_country", "region", "dc_plant"],
|
||||||
|
"machine_fields": ["mtm", "machine_sn", "machine_type", "model"],
|
||||||
|
"service_fields": ["whether_premier", "stm_planner", "category", "lenovo_ref_no", "case_number", "key_lenovo_ref_no", "service_type"],
|
||||||
|
"status_fields": ["status", "status_date"],
|
||||||
|
"additional_fields": ["parts_sales", "warranty", "aging_day", "action", "aging_range", "recovery_day", "recovery_range", "order_type", "customer", "life_cycle", "hawb", "bol", "dn", "po", "prid"],
|
||||||
|
"system_filter_fields": ["is_passdue", "is_dummy", "data_flag"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"business_logic_rules": {
|
||||||
|
"soid_or_service_order_id": "禁止使用WHERE soid = 'xxx'或WHERE service_order_id = 'xxx'单独过滤,必须使用(service_order_id = 'xxx' or soid = 'xxx')进行过滤",
|
||||||
|
"sorting": "默认使用如下字段排序: soid, eta desc",
|
||||||
|
"alias_usage": "WHERE子句中支持使用字段别名进行查询,SELECT子句中使用原始字段名或别名均可",
|
||||||
|
"date_conversion": "所有日期字段必须使用cast(column as varchar(4096)) as column转为字符类型",
|
||||||
|
"default_filters": ["is_dummy = '0'", "data_flag = 'Newest'"],
|
||||||
|
"null_handling": "日期字段转换时保留NULL值",
|
||||||
|
"model_query_rule": "查询model字段时必须使用like匹配,如: model like '%X13%'",
|
||||||
|
"field_extraction_rule": "使用正则表达式和关键词匹配提取用户明确提到的字段,建立字段别名映射表",
|
||||||
|
"field_order_rule": "用户提到的字段按问题中出现顺序排列在最前面,然后补充默认展示字段中未提及的字段",
|
||||||
|
"topmost_pn and part_number_distinction": "确保区分topmost_pn(tp,tm,topmost)和part_number(pn)字段,避免混淆",
|
||||||
|
"todays_date_handling": "当用户查询包含“今天”或“当前日期”的信息时,替换为系统当前日期进行过滤"
|
||||||
|
},
|
||||||
|
"field_mapping_reference": {
|
||||||
|
"critical_note": "此表包含最新状态数据和历史变更数据的联合,通过data_flag区分。最新数据(Newest)每条订单只有一条记录,历史数据(Changelog)每条订单有多条记录。所有日期字段在查询时自动转换为字符类型。请注意检查用户是否使用了字段的alias,不要忽略!!!系统字段(is_passdue, data_flag)仅用于WHERE过滤,不显示在SELECT结果中。用户提到的字段应优先显示在SELECT子句最前面。",
|
||||||
|
"critical_note1": {"type":"字段类型", "desc":"字段描述", "format": "数据格式", "query_format": "查询时数据格式" ,"example":"字段示例值", "values":"枚举值", "alias":"字段别名"},
|
||||||
|
"key_identifiers": {
|
||||||
|
"service_order_id": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "主订单号,唯一标识, 简写为SO 或 WO ,10位码值,注意与soid(12位码值)区分",
|
||||||
|
"example": "4020438779",
|
||||||
|
"alias": ["so","SO", "wo", "订单号", "主单号", "SO", "服务订单", "order_no", "Service Order", "BO", "backorder"]
|
||||||
|
},
|
||||||
|
"soid": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "服务订单明细,12位码值,注意与service_order_id(10位码值)区分",
|
||||||
|
"example": "402043877920",
|
||||||
|
"alias": ["明细ID", "SOID", "订单明细", "服务订单明细", "Service Order Item Detail", "SO ID"]
|
||||||
|
},
|
||||||
|
"part_number": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "原始申请物料号, 注意与topmost pn区分, 这不是topmost pn, 这是part number, PN",
|
||||||
|
"example": "5CB1L57599",
|
||||||
|
"alias": ["pn", "part", "parts", "物料号", "零件号", "Part Number", "材料号", "物料编码", "零件编码"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"milestone_date_fields": {
|
||||||
|
"service_order_creation_date": {
|
||||||
|
"type": "datetime",
|
||||||
|
"desc": "开单日期",
|
||||||
|
"format": "YYYY-MM-DD HH:MM:SS",
|
||||||
|
"query_format": "作为字符串展示",
|
||||||
|
"alias": ["create_date", "创建时间", "下单时间", "开单日期", "SO创建时间", "开单时间", "Order Creation Date", "created_date", "创建日期"]
|
||||||
|
},
|
||||||
|
"eta": {
|
||||||
|
"type": "date",
|
||||||
|
"desc": "物料预计到达日期(Estimated Time of Arrival)",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"query_format": "作为字符串展示",
|
||||||
|
"alias": ["预计到达", "计划到达时间", "ETA", "预计到港时间", "预计到货时间", "预计送达时间", "物料ETA"]
|
||||||
|
},
|
||||||
|
"so_eta": {
|
||||||
|
"type": "date",
|
||||||
|
"desc": "销售订单层预计到达时间(Service Order ETA)",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"query_format": "作为字符串展示",
|
||||||
|
"alias": ["SO预计到达", "订单ETA", "服务订单预计到达", "Service Order ETA", "SO ETA", "销售订单ETA"]
|
||||||
|
},
|
||||||
|
"status_date": {
|
||||||
|
"type": "date",
|
||||||
|
"desc": "状态日期, 禁止使用status_date字段排序, 禁止使用status_date字段排序, 禁止使用status_date字段排序",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"query_format": "作为字符串展示",
|
||||||
|
"alias": ["status date", "状态时间", "状态更新时间", "最后更新", "状态日期", "Status Update Date", "更新时间", "更新日期"]
|
||||||
|
},
|
||||||
|
"update_date": {
|
||||||
|
"type": "date",
|
||||||
|
"desc": "历史数据状态更新日期,表示该条历史记录的生成时间",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"query_format": "作为字符串展示",
|
||||||
|
"alias": ["更新时间", "更新日期", "历史数据状态更新日期", "修改时间"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"additional_fields": {
|
||||||
|
"topmost_pn": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "最紧缺物料号, 注意与pn区分, 这是topmost pn, 这是topmost pn, 这是topmost pn, 不是PN",
|
||||||
|
"example": "5CB1L57599",
|
||||||
|
"alias": ["tp","tm", "最紧缺物料号", "Topmost Part Number", "topmost", "topmost pn"]
|
||||||
|
},
|
||||||
|
"commodity_code": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "商品编码",
|
||||||
|
"example": "PL",
|
||||||
|
"alias": ["commodity", "商品代码", "编码", "cc", "物料分类", "Commodity Code", "商品类别", "物料类型", "商品编码"]
|
||||||
|
},
|
||||||
|
"ship_to_country": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "收货国家",
|
||||||
|
"values": ["VN","AU","IN","HK","PH","KR","ID","TW","MO","FJ","LK","MY","SG","NZ","TH","JP","BD"],
|
||||||
|
"alias": ["country", "国家", "收货国", "目的地国家", "Ship to Country", "目标国家", "送达国家", "收货地点", "目的地"]
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "地区",
|
||||||
|
"values": ["CAP", "ANZ", "India", "JP"],
|
||||||
|
"alias": ["区域", "大区", "Region Code", "地域代码", "地区代码", "地理区域"]
|
||||||
|
},
|
||||||
|
"dc_plant": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "配送中心/工厂",
|
||||||
|
"example": "VN01",
|
||||||
|
"alias": ["dc", "工厂", "配送中心", "发货配送中心", "DC", "发货中心", "Distribution Center", "发货工厂", "配送点", "仓库"]
|
||||||
|
},
|
||||||
|
"mtm": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "机器型号,请注意与mt区分,这不是mt,这是mtm,这是mtm,这是mtm",
|
||||||
|
"example": "20QUS0SQ00",
|
||||||
|
"alias": ["mtm", "机型号", "型号", "Machine Type Model", "设备型号", "机器型号代码", "机型代码", "MTM"]
|
||||||
|
},
|
||||||
|
"machine_sn": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "机器序列号, MTM SN, MT SN都是machine_sn ,请注意与mtm区分,这不是mtm也不是MT, 这是sn,这是sn,这是sn",
|
||||||
|
"example": "PW01554B",
|
||||||
|
"alias": ["sn", "serial_number", "序列号", "SN", "Machine Serial Number", "设备序列号", "机器SN", "设备SN", "序列编号", "MTM SN", "MT SN"]
|
||||||
|
},
|
||||||
|
"machine_type": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "机器类型, 请注意与mtm区分,这不是mtm,这是mt,这是mt,这是mt",
|
||||||
|
"example": "20Q1",
|
||||||
|
"alias": ["MT", "machine type", "机器类型", "设备类型", "机型大类", "机器大类"]
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "Lenovo的机型, 禁止使用model = 'model_name', 查询时必须转换为小写并使用like匹配,如: lower(model) like '%X13%'",
|
||||||
|
"example": ["Yoga 7 16IAH7", "X13 GEN3", "T16 Gen4 AMD", "Legion 7 16ACHg6"],
|
||||||
|
"alias": ["model", "机型", "型号", "Model", "设备型号", "机器型号"]
|
||||||
|
},
|
||||||
|
"lenovo_ref_no": {
|
||||||
|
"type": "varchar(63355)",
|
||||||
|
"desc": "联想内部参考号",
|
||||||
|
"alias": ["Lenovo Ref No", "ref_no", "联想单号", "参考号", "联想内部参考号", "内部参考号", "Lenovo Reference", "联想内部编号", "参考编号", "参考单号"]
|
||||||
|
},
|
||||||
|
"case_number": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "事件号/案例号",
|
||||||
|
"example": "CS12345678",
|
||||||
|
"alias": ["case", "事件号", "事件编号", "case no", "Case Number", "lenovo case", "工单号", "服务请求号"]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "订单状态",
|
||||||
|
"example": ["wrong order", "DN", "ETA", "Standard Good Stock"],
|
||||||
|
"alias": ["状态", "订单状态", "Status", "当前状态", "物料状态", "订单最新状态"]
|
||||||
|
},
|
||||||
|
"whether_premier": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "优先级标识,默认查询 'Premier'",
|
||||||
|
"values": ["Starndard", "Premier"],
|
||||||
|
"alias": ["premier", "优质服务", "是否优质", "是否Premier", "Premier服务", "是否优先", "Whether Premier", "优先服务标识", "VIP服务"]
|
||||||
|
},
|
||||||
|
"stm_planner": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "短期物料计划员",
|
||||||
|
"alias": ["planner", "计划员姓名", "STM计划员", "STM Planner", "服务计划员", "运输计划员", "物料计划员", "计划员"]
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "订单分类,查询时必须转换为小写并使用like匹配, 如: lower(category) like '%standard%'",
|
||||||
|
"example": "Standard",
|
||||||
|
"alias": ["类型", "分类", "订单类别", "Category", "服务类别", "订单分类", "分类类型", "服务类型"]
|
||||||
|
},
|
||||||
|
"parts_sales": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "零件销售信息",
|
||||||
|
"alias": ["零件销售", "销售信息", "部件销售", "配件销售", "销售数据"]
|
||||||
|
},
|
||||||
|
"warranty": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "保修信息",
|
||||||
|
"alias": ["保修", "保修信息", "质保", "保修条款", "保修状态"]
|
||||||
|
},
|
||||||
|
"aging_day": {
|
||||||
|
"type": "int",
|
||||||
|
"desc": "订单从生成(order_creation_date)到当前(update_date)所经过的具体天数",
|
||||||
|
"examples": [118, 77, 12],
|
||||||
|
"alias": ["账龄天数", "账龄", "aging day", "age days", "账龄分类", "天数分类", "Aged", "Age"]
|
||||||
|
},
|
||||||
|
"aging_range":{
|
||||||
|
"type": "varchar(2048)",
|
||||||
|
"desc": "订单从生成(order_creation_date)到当前(update_date)所经过的具体天数区间,并按照星期分类",
|
||||||
|
"values": ["0-7D", "8-14D", "15-21D", "22-28D", "28D+", "null"],
|
||||||
|
"alias": ["账龄天数", "账龄", "aging day", "age date range", "账龄分类", "天数分类"]
|
||||||
|
},
|
||||||
|
"recovery_day": {
|
||||||
|
"type": "int",
|
||||||
|
"desc": "订单从Update_date到SO_ETA所经过的具体天数",
|
||||||
|
"examples": [118, 77, 12],
|
||||||
|
"alias": ["账龄天数", "账龄", "recovery day", "账期分类", "账龄分类", "天数分类", "Aged", "Age"]
|
||||||
|
},
|
||||||
|
"recovery_range": {
|
||||||
|
"type": "varchar(2048)",
|
||||||
|
"desc": "订单从Update_date到SO_ETA所经过的具体天数区间,按照星期分类",
|
||||||
|
"values": ["0-7D", "8-14D", "15-21D", "22-28D", "28D+", "null"],
|
||||||
|
"alias": ["账龄天数", "账龄", "recovery range", "账期分类", "账龄分类", "天数分类"]
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "操作/规则",
|
||||||
|
"example": "Not accept sub",
|
||||||
|
"alias": ["sub_rule", "操作", "规则", "限制规则", "操作类型", "处理规则"]
|
||||||
|
},
|
||||||
|
"order_type":{
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "订单类型",
|
||||||
|
"values": ["MDOA", "OPT", "CLW", "DOA", "FOC", "OOW", "OBL", "ADW"],
|
||||||
|
"alias": ["type", "订单类别", "类别", "订单类型", "Order Type", "服务类型", "服务类别"]
|
||||||
|
},
|
||||||
|
"key_lenovo_ref_no": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "关键联想参考号",
|
||||||
|
"alias": ["关键参考号", "主要参考号", "关键联想单号"]
|
||||||
|
},
|
||||||
|
"service_type": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "服务类型",
|
||||||
|
"alias": ["服务类型", "服务种类", "service type"]
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "客户信息",
|
||||||
|
"alias": ["客户", "客户名称", "客户信息", "customer name"]
|
||||||
|
},
|
||||||
|
"life_cycle": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "TM生命周期",
|
||||||
|
"alias": ["tm_lifecycle","TM生命周期", "生命周期阶段", "产品生命周期"]
|
||||||
|
},
|
||||||
|
"hawb": {
|
||||||
|
"type": "varchar(4096)",
|
||||||
|
"desc": "house air way bill",
|
||||||
|
"examples": ["DIM042186899"],
|
||||||
|
"alias": ["分运单号", "运单号", "代理运单", "分运单", "house air way bill"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"system_flag_fields": {
|
||||||
|
"is_passdue": {
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"desc": "是否逾期:'1'表示逾期,'0'表示正常",
|
||||||
|
"business_rule": "根据用户要求过滤, 禁止出现在SELECT中",
|
||||||
|
"display_rule": "仅用于WHERE过滤,不显示",
|
||||||
|
"alias": ["passdue", "逾期标志", "是否逾期", "overdue", "超期标志", "逾期标识"]
|
||||||
|
},
|
||||||
|
"is_dummy": {
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"desc": "是否为虚拟/测试数据:'1'表示是dummy数据,'0'表示真实数据",
|
||||||
|
"business_rule": "默认过滤掉dummy数据(is_dummy='0'),除非用户指定, 禁止出现在SELECT中",
|
||||||
|
"display_rule": "仅用于WHERE过滤,不显示",
|
||||||
|
"alias": ["dummy", "虚拟标志", "是否虚拟", "测试数据", "假数据标识", "模拟数据"]
|
||||||
|
},
|
||||||
|
"data_flag": {
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"desc": "数据标志:'Newest'表示最新状态,'Changelog'表示历史变更",
|
||||||
|
"example": "Newest",
|
||||||
|
"business_rule": "默认查询最新状态(Newest),查询历史时使用Changelog, 禁止出现在SELECT中",
|
||||||
|
"display_rule": "仅用于WHERE过滤,不显示",
|
||||||
|
"alias": ["数据标志", "数据类型", "数据状态", "记录类型", "数据分类"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"newest_status_all_fields": {
|
||||||
|
"user": "请查询SO:4020986743的全部字段",
|
||||||
|
"sql": "SELECT service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE (service_order_id = '4020986743' or soid = '4020986743') AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "默认查询最新状态,按默认展示字段顺序显示所有字段(除系统字段),日期字段转换为字符类型"
|
||||||
|
},
|
||||||
|
"history_records_all_fields": {
|
||||||
|
"user": "查看2025年12月31的so为4020438779的历史变更记录",
|
||||||
|
"sql": "SELECT service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE (service_order_id = '4020438779' or soid = '4020438779') AND cast(update_date as varchar(4096)) like '%2025-12-31%' AND is_dummy = '0' AND data_flag = 'Changelog' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "查询历史变更记录,按默认展示字段顺序显示所有字段(除系统字段),日期字段转换为字符类型"
|
||||||
|
},
|
||||||
|
"specific_fields_query": {
|
||||||
|
"user": "查询SO为4020438779的country, eta, order_type",
|
||||||
|
"sql": "SELECT ship_to_country, cast(eta as varchar(4096)) as eta, order_type, service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, part_number, topmost_pn, commodity_code, category, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE (service_order_id = '4020438779' or soid = '4020438779') AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "用户提到的字段(ship_to_country, eta, order_type)优先显示在最前面,然后补充其他默认展示字段"
|
||||||
|
},
|
||||||
|
"aggragate_query": {
|
||||||
|
"user": "by country, key_Lenovo Ref No, eta 汇总REGION=CAP passdue 信息, 并从小到大排序",
|
||||||
|
"sql": "SELECT region, ship_to_country, key_lenovo_ref_no, cast(eta as varchar(4096)) as eta, count(1) FROM dwd_ai.apbo_eta_ful WHERE is_passdue = '1' AND is_dummy = '0' AND data_flag = 'Newest' AND region = 'CAP' group by region, ship_to_country, key_lenovo_ref_no, cast(eta as varchar(4096)) ORDER BY count(1) ASC",
|
||||||
|
"field_selection_reason": "查询特定账龄的订单,按默认展示字段顺序显示,日期字段转换为字符类型"
|
||||||
|
},
|
||||||
|
"overdue_orders_all_fields": {
|
||||||
|
"user": "查询所有passdue/逾期的订单",
|
||||||
|
"sql": "SELECT service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE is_passdue = '1' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "查询逾期订单,按默认展示字段顺序显示,日期字段转换为字符类型"
|
||||||
|
},
|
||||||
|
"Machine_sn_query": {
|
||||||
|
"user": "SN PF4C8CBL的order 信息",
|
||||||
|
"sql": "SELECT machine_sn, service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE machine_sn = 'PF4C8CBL' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "用户提到的字段(machine_sn)优先显示在最前面,然后补充其他默认展示字段"
|
||||||
|
},
|
||||||
|
"status_query": {
|
||||||
|
"user": "查询状态为wrong order的订单",
|
||||||
|
"sql": "SELECT status, service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE status = 'wrong order' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "用户提到的字段(status)优先显示在最前面,然后补充其他默认展示字段"
|
||||||
|
},
|
||||||
|
"multiple_fields_query": {
|
||||||
|
"user": "查看SN为PF4C8CBL的订单状态和机器型号",
|
||||||
|
"sql": "SELECT machine_sn, status, model, service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE machine_sn = 'PF4C8CBL' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "用户提到的字段(machine_sn, status, model)按问题中出现的顺序优先显示在最前面,然后补充其他默认展示字段"
|
||||||
|
},
|
||||||
|
"model_query_all_fields": {
|
||||||
|
"user": "查询机型包含X13的订单",
|
||||||
|
"sql": "SELECT service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE model like '%X13%' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "查询特定机型的订单,使用like匹配model字段,按默认展示字段顺序显示"
|
||||||
|
},
|
||||||
|
"order_type_query_all_fields": {
|
||||||
|
"user": "查询订单类型为MDOA的订单",
|
||||||
|
"sql": "SELECT service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, region, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE order_type = 'MDOA' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "查询特定订单类型的订单,按默认展示字段顺序显示"
|
||||||
|
},
|
||||||
|
"passdue_info_query": {
|
||||||
|
"user": "region = ANZ 的passdue 信息",
|
||||||
|
"sql": "SELECT region, service_order_id, soid, cast(service_order_creation_date as varchar(4096)) as service_order_creation_date, ship_to_country, part_number, topmost_pn, commodity_code, category, cast(eta as varchar(4096)) as eta, cast(so_eta as varchar(4096)) as so_eta, key_lenovo_ref_no, order_type, stm_planner, dc_plant, mtm, machine_sn, whether_premier, service_type, warranty, parts_sales, customer, life_cycle, model, aging_range, lenovo_ref_no, action, cast(update_date as varchar(4096)) as update_date FROM dwd_ai.apbo_eta_ful WHERE is_passdue = '1' AND is_dummy = '0' AND data_flag = 'Newest' ORDER BY soid, eta DESC",
|
||||||
|
"field_selection_reason": "查询逾期订单的详细信息,按默认展示字段顺序显示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"table": "example_table",
|
||||||
|
"description": "示例表模型提示词",
|
||||||
|
"system_prompt": "You are an expert SQL generator.",
|
||||||
|
"business_prompt": "Generate SQL for example_table based on the user's intent.",
|
||||||
|
"constraints": [
|
||||||
|
"Use only fields defined in this table.",
|
||||||
|
"Return only SQL without explanations."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
本目录存放业务表模型元数据提示词(JSON 格式)。
|
||||||
|
|
||||||
|
约定:每个 JSON 文件对应一个数据库表模型,描述表与字段元数据。
|
||||||
|
示例文件:order_metadata.json、customer_metadata.json 等。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"table": "example_table",
|
||||||
|
"description": "示例表模型元数据提示词",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"description": "主键"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"description": "创建时间"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": [],
|
||||||
|
"notes": [
|
||||||
|
"字段含义与业务规则可在此补充"
|
||||||
|
]
|
||||||
|
}
|
||||||
+32
-17
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Basic usage examples for the LangChain + LangGraph scaffolding
|
LangChain + LangGraph 脚手架的基础用法示例
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -11,57 +11,72 @@ from workflows.workflow_manager import WorkflowManager, WorkflowType
|
|||||||
|
|
||||||
|
|
||||||
def example_conversation():
|
def example_conversation():
|
||||||
"""Example of using the conversation workflow"""
|
"""对话工作流示例"""
|
||||||
print("=== Conversation Workflow Example ===")
|
print("=== Conversation Workflow Example ===")
|
||||||
|
|
||||||
|
# 使用 config.ini 中的默认模型
|
||||||
manager = WorkflowManager()
|
manager = WorkflowManager()
|
||||||
|
|
||||||
# First message
|
# 第一条消息
|
||||||
result1 = manager.execute_workflow(
|
result1 = manager.execute_workflow(
|
||||||
WorkflowType.CONVERSATION,
|
WorkflowType.CONVERSATION,
|
||||||
"Hello! Can you help me with some calculations?"
|
"你好,你能帮我做什么?"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Session ID: {result1['session_id']}")
|
print(f"Session ID: {result1['session_id']}")
|
||||||
print(f"Response: {result1['result']['messages'][-1].content}")
|
print(f"Response: {result1['result']['messages'][-1].content}")
|
||||||
|
|
||||||
# Second message in the same session
|
# 同一会话中的第二条消息
|
||||||
result2 = manager.execute_workflow(
|
result2 = manager.execute_workflow(
|
||||||
WorkflowType.CONVERSATION,
|
WorkflowType.CONVERSATION,
|
||||||
"What can you help me with?",
|
"你的优势是什么?",
|
||||||
session_id=result1['session_id']
|
session_id=result1['session_id']
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Second response: {result2['result']['messages'][-1].content}")
|
print(f"Second response: {result2['result']['messages'][-1].content}")
|
||||||
print("\n")
|
|
||||||
|
# 使用不同模型的示例
|
||||||
|
# print("\n--- Using a different model (gpt-3.5-turbo) ---")
|
||||||
|
# try:
|
||||||
|
# manager_alt_model = WorkflowManager(default_model_section='gpt-3.5-turbo')
|
||||||
|
# result_alt = manager_alt_model.execute_workflow(
|
||||||
|
# WorkflowType.CONVERSATION,
|
||||||
|
# "Hi, what model are you?"
|
||||||
|
# )
|
||||||
|
# print(f"Response from gpt-3.5-turbo: {result_alt['result']['messages'][-1].content}")
|
||||||
|
# except ValueError as e:
|
||||||
|
# print(f"Could not run alternate model example: {e}")
|
||||||
|
# print("Please ensure you have a [gpt-3.5-turbo] section in your config.ini")
|
||||||
|
#
|
||||||
|
# print("\n")
|
||||||
|
|
||||||
|
|
||||||
def example_tool_usage():
|
def example_tool_usage():
|
||||||
"""Example of using the tool workflow"""
|
"""工具工作流示例"""
|
||||||
print("=== Tool Workflow Example ===")
|
print("=== Tool Workflow Example ===")
|
||||||
|
|
||||||
manager = WorkflowManager()
|
manager = WorkflowManager()
|
||||||
|
|
||||||
# Use calculator tool
|
# 使用计算器工具
|
||||||
result = manager.execute_workflow(
|
result = manager.execute_workflow(
|
||||||
WorkflowType.TOOL_USING,
|
WorkflowType.TOOL_USING,
|
||||||
"Calculate 25 * 4 + 10"
|
"计算 25 * 4 + 10"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Session ID: {result['session_id']}")
|
print(f"Session ID: {result['session_id']}")
|
||||||
|
|
||||||
# Extract tool messages and responses
|
# 提取工具消息与回复
|
||||||
for message in result['result']['messages']:
|
for message in result['result']['messages']:
|
||||||
if hasattr(message, 'tool_calls') and message.tool_calls:
|
if hasattr(message, 'tool_calls') and message.tool_calls:
|
||||||
print(f"Tool call: {message.tool_calls}")
|
print(f"Tool call: {message.tool_calls}")
|
||||||
elif hasattr(message, 'content'):
|
elif hasattr(message, 'content') and message.content:
|
||||||
print(f"Response: {message.content}")
|
print(f"Response: {message.content}")
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
def list_available_workflows():
|
def list_available_workflows():
|
||||||
"""List all available workflows"""
|
"""列出可用工作流"""
|
||||||
print("=== Available Workflows ===")
|
print("=== Available Workflows ===")
|
||||||
|
|
||||||
manager = WorkflowManager()
|
manager = WorkflowManager()
|
||||||
@@ -74,14 +89,14 @@ def list_available_workflows():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Check if configuration is valid
|
# 检查配置是否有效
|
||||||
try:
|
try:
|
||||||
from config import Config
|
from config import Config
|
||||||
Config.validate_config()
|
Config.validate_config()
|
||||||
print("✅ Configuration is valid")
|
print("✅ Configuration is valid")
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# Run examples
|
# 运行示例
|
||||||
list_available_workflows()
|
list_available_workflows()
|
||||||
example_conversation()
|
example_conversation()
|
||||||
example_tool_usage()
|
example_tool_usage()
|
||||||
@@ -89,6 +104,6 @@ if __name__ == "__main__":
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Configuration error: {e}")
|
print(f"❌ Configuration error: {e}")
|
||||||
print("\nPlease make sure to:")
|
print("\nPlease make sure to:")
|
||||||
print("1. Copy .env.example to .env")
|
print("1. Copy 'config/config.ini.example' to 'config/config.ini'")
|
||||||
print("2. Set your OPENAI_API_KEY in the .env file")
|
print("2. Set your API key in the 'config/config.ini' file")
|
||||||
print("3. Install dependencies: pip install -r requirements.txt")
|
print("3. Install dependencies: pip install -r requirements.txt")
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Main entry point for the LangChain + LangGraph scaffolding project
|
LangChain + LangGraph 脚手架项目主入口
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -9,11 +9,14 @@ from workflows.workflow_manager import WorkflowManager, WorkflowType
|
|||||||
|
|
||||||
|
|
||||||
def interactive_cli():
|
def interactive_cli():
|
||||||
"""Interactive command-line interface"""
|
"""交互式命令行界面"""
|
||||||
print("🚀 LangChain + LangGraph Scaffolding")
|
print("🚀 LangChain + LangGraph Scaffolding")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
manager = WorkflowManager()
|
# 允许通过命令行指定模型配置段,例如 python main.py gpt-3.5-turbo
|
||||||
|
model_section = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
|
|
||||||
|
manager = WorkflowManager(default_model_section=model_section)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
print("\nAvailable workflows:")
|
print("\nAvailable workflows:")
|
||||||
@@ -37,7 +40,7 @@ def interactive_cli():
|
|||||||
print("Invalid choice")
|
print("Invalid choice")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Interactive session
|
# 交互会话
|
||||||
session_id = None
|
session_id = None
|
||||||
while True:
|
while True:
|
||||||
user_input = input("\nYou: ").strip()
|
user_input = input("\nYou: ").strip()
|
||||||
@@ -57,12 +60,12 @@ def interactive_cli():
|
|||||||
|
|
||||||
session_id = result['session_id']
|
session_id = result['session_id']
|
||||||
|
|
||||||
# Extract and display the response
|
# 提取并显示回复
|
||||||
last_message = result['result']['messages'][-1]
|
last_message = result['result']['messages'][-1]
|
||||||
if hasattr(last_message, 'content'):
|
if hasattr(last_message, 'content') and last_message.content:
|
||||||
print(f"AI: {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:
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
print(f"🔧 Tools used: {[tc['name'] for tc in last_message.tool_calls]}")
|
print(f"🔧 Tools used: {[tc['name'] for tc in last_message.tool_calls]}")
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ def interactive_cli():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function"""
|
"""主函数"""
|
||||||
try:
|
try:
|
||||||
from config import Config
|
from config import Config
|
||||||
Config.validate_config()
|
Config.validate_config()
|
||||||
@@ -85,8 +88,8 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Configuration error: {e}")
|
print(f"❌ Configuration error: {e}")
|
||||||
print("\nPlease make sure to:")
|
print("\nPlease make sure to:")
|
||||||
print("1. Copy .env.example to .env")
|
print("1. Copy 'config/config.ini.example' to 'config/config.ini'")
|
||||||
print("2. Set your OPENAI_API_KEY in the .env file")
|
print("2. Set your API key in the 'config/config.ini' file")
|
||||||
print("3. Install dependencies: pip install -r requirements.txt")
|
print("3. Install dependencies: pip install -r requirements.txt")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -1,6 +1,11 @@
|
|||||||
langchain-core>=0.3.0
|
langchain-core>=1.2.6
|
||||||
langchain>=1.2.1
|
langchain>=1.2.1
|
||||||
langgraph>=0.2.0
|
langgraph>=1.0.5
|
||||||
langchain-openai>=0.2.0
|
langchain-openai>=1.1.6
|
||||||
python-dotenv>=1.0.0
|
pydantic>=2.0.0
|
||||||
pydantic>=2.0.0
|
fastapi>=0.110.0
|
||||||
|
uvicorn>=0.30.0
|
||||||
|
nacos-sdk-python>=2.0.9
|
||||||
|
httpx>=0.27.0
|
||||||
|
pyyaml>=6.0.1
|
||||||
|
redis>=5.0.0
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AgentInput(BaseModel):
|
||||||
|
"""Agent 输入模型"""
|
||||||
|
input: str
|
||||||
|
session_id: Optional[str] = None
|
||||||
|
workflow_type: str = "conversation"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AgentOutput(BaseModel):
|
||||||
|
"""Agent 输出模型"""
|
||||||
|
session_id: str
|
||||||
|
workflow_type: str
|
||||||
|
result: dict
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from typing import Any
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ToolInput(BaseModel):
|
||||||
|
"""工具调用请求体"""
|
||||||
|
tool_name: str
|
||||||
|
payload: Any
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ToolOutput(BaseModel):
|
||||||
|
"""工具调用响应体"""
|
||||||
|
ok: bool
|
||||||
|
data: Optional[Any] = None
|
||||||
|
error: Optional[str] = None
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def load_templates(dir_path: str) -> List[Dict[str, any]]:
|
||||||
|
items = []
|
||||||
|
for name in os.listdir(dir_path):
|
||||||
|
if not name.endswith(".json"):
|
||||||
|
continue
|
||||||
|
with open(os.path.join(dir_path, name), "r", encoding="utf-8") as f:
|
||||||
|
items.append(json.load(f))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def build_document(item: Dict[str, any]) -> str:
|
||||||
|
table = item.get("table", "")
|
||||||
|
templates = item.get("templates", [])
|
||||||
|
lines = [f"table: {table}"]
|
||||||
|
for t in templates:
|
||||||
|
lines.append(f"- {t}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cfg = Config.get_section("ragflow")
|
||||||
|
base_url = cfg.get("url", "").rstrip("/")
|
||||||
|
api_key = cfg.get("api_key", "")
|
||||||
|
dataset_ids = cfg.get("dataset_ids", "")
|
||||||
|
|
||||||
|
upload_path = cfg.get("upload", "")
|
||||||
|
if not upload_path:
|
||||||
|
raise RuntimeError("未配置 ragflow.upload 上传接口,请在 config/config.ini 中设置")
|
||||||
|
|
||||||
|
url = base_url + "/" + upload_path.lstrip("/")
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
||||||
|
|
||||||
|
root = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
templates_dir = os.path.join(root, "config", "ragflow_templates")
|
||||||
|
items = load_templates(templates_dir)
|
||||||
|
|
||||||
|
payload = []
|
||||||
|
for item in items:
|
||||||
|
payload.append(
|
||||||
|
{
|
||||||
|
"dataset_ids": dataset_ids,
|
||||||
|
"content": build_document(item),
|
||||||
|
"metadata": {"table": item.get("table")},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with httpx.Client(timeout=60) as client:
|
||||||
|
response = client.post(url, json={"documents": payload}, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
print("同步完成")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from workflows.workflow_manager import WorkflowManager
|
||||||
|
from services.nacos_service import load_nacos_config, load_service_config, NacosManager
|
||||||
|
from services.tool_router import ToolRouter
|
||||||
|
from api import endpoints
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""创建 FastAPI 应用"""
|
||||||
|
service_config = load_service_config()
|
||||||
|
nacos_config = load_nacos_config()
|
||||||
|
default_model_section = Config._config.get("app", "model_section", fallback=Config.DEFAULT_MODEL_SECTION)
|
||||||
|
|
||||||
|
workflow_manager = WorkflowManager(default_model_section=default_model_section)
|
||||||
|
nacos_manager = NacosManager(nacos_config=nacos_config, service_config=service_config)
|
||||||
|
tool_router = ToolRouter()
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""应用生命周期管理"""
|
||||||
|
logger.info("🚀 服务启动中...")
|
||||||
|
app.state.workflow_manager = workflow_manager
|
||||||
|
app.state.nacos_manager = nacos_manager
|
||||||
|
app.state.service_config = service_config
|
||||||
|
app.state.tool_router = tool_router
|
||||||
|
|
||||||
|
await nacos_manager.start()
|
||||||
|
logger.info("✅ 服务准备就绪: %s on %s:%s", service_config.service_name, service_config.ip, service_config.port)
|
||||||
|
yield
|
||||||
|
logger.info("🛑 服务关闭中...")
|
||||||
|
await nacos_manager.stop()
|
||||||
|
logger.info("✅ 服务已关闭")
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="More Dots API",
|
||||||
|
description="More Dots 工作流服务",
|
||||||
|
version=service_config.metadata.get("version", "1.0.0"),
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(endpoints.router)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_config = load_service_config()
|
||||||
|
uvicorn.run(
|
||||||
|
"server:app",
|
||||||
|
host=service_config.host,
|
||||||
|
port=service_config.port,
|
||||||
|
reload=False,
|
||||||
|
log_level="info",
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
except Exception:
|
||||||
|
redis = None
|
||||||
|
|
||||||
|
|
||||||
|
class CacheBase:
|
||||||
|
"""缓存接口"""
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: int) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoopCache(CacheBase):
|
||||||
|
"""空实现缓存"""
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RedisCache(CacheBase):
|
||||||
|
"""Redis 缓存"""
|
||||||
|
|
||||||
|
def __init__(self, url: str, db: int = 0):
|
||||||
|
if redis is None:
|
||||||
|
raise ImportError("未安装 redis 依赖")
|
||||||
|
self._client = redis.Redis.from_url(url, db=db, decode_responses=True)
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
return self._client.get(key)
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: int) -> None:
|
||||||
|
self._client.set(key, value, ex=ttl)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def create_chat_model(model_section: Optional[str] = None) -> ChatOpenAI:
|
||||||
|
"""创建 LLM 实例"""
|
||||||
|
model_config = Config.get_model_config(model_section)
|
||||||
|
return ChatOpenAI(
|
||||||
|
model=model_config['model'],
|
||||||
|
api_key=model_config['api_key'],
|
||||||
|
base_url=model_config.get('base_url'),
|
||||||
|
temperature=0.1,
|
||||||
|
max_retries=Config.MAX_RETRIES,
|
||||||
|
timeout=Config.TIMEOUT
|
||||||
|
)
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nacos
|
||||||
|
except Exception:
|
||||||
|
nacos = None
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NacosConfig:
|
||||||
|
"""Nacos 配置"""
|
||||||
|
enabled: bool
|
||||||
|
server_addresses: str
|
||||||
|
namespace: str
|
||||||
|
group_name: str
|
||||||
|
cluster_name: str
|
||||||
|
username: Optional[str]
|
||||||
|
password: Optional[str]
|
||||||
|
heartbeat_interval: int
|
||||||
|
weight: float
|
||||||
|
ephemeral: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServiceConfig:
|
||||||
|
"""服务配置"""
|
||||||
|
service_name: str
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
ip: str
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_ip() -> str:
|
||||||
|
"""获取本地 IP 地址"""
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取本地 IP 失败,使用 127.0.0.1: {e}")
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def load_nacos_config() -> NacosConfig:
|
||||||
|
"""从 config.ini 读取 Nacos 配置"""
|
||||||
|
section = "nacos"
|
||||||
|
enabled = Config._config.getboolean(section, "enabled", fallback=False)
|
||||||
|
return NacosConfig(
|
||||||
|
enabled=enabled,
|
||||||
|
server_addresses=Config._config.get(section, "server", fallback="localhost:8848"),
|
||||||
|
namespace=Config._config.get(section, "namespace", fallback="public"),
|
||||||
|
group_name=Config._config.get(section, "group_name", fallback="DEFAULT_GROUP"),
|
||||||
|
cluster_name=Config._config.get(section, "cluster_name", fallback="DEFAULT"),
|
||||||
|
username=Config._config.get(section, "username", fallback="") or None,
|
||||||
|
password=Config._config.get(section, "password", fallback="") or None,
|
||||||
|
heartbeat_interval=Config._config.getint(section, "heartbeat_interval", fallback=5),
|
||||||
|
weight=Config._config.getfloat(section, "weight", fallback=1.0),
|
||||||
|
ephemeral=Config._config.getboolean(section, "ephemeral", fallback=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_service_config() -> ServiceConfig:
|
||||||
|
"""从 config.ini 读取服务配置"""
|
||||||
|
section = "app"
|
||||||
|
service_name = Config._config.get(section, "service_name", fallback="more-dots-api")
|
||||||
|
host = Config._config.get(section, "host", fallback="0.0.0.0")
|
||||||
|
port = Config._config.getint(section, "port", fallback=8000)
|
||||||
|
ip = host if host != "0.0.0.0" else _get_local_ip()
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"version": Config._config.get(section, "version", fallback="1.0.0"),
|
||||||
|
"service_type": "fastapi",
|
||||||
|
"api_paths": "/health,/api/workflows,/api/workflows/stream,/nacos/status",
|
||||||
|
"streaming": "false",
|
||||||
|
"model_section": Config._config.get(section, "model_section", fallback=Config.DEFAULT_MODEL_SECTION),
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_meta = Config.get_section("metadata")
|
||||||
|
if extra_meta:
|
||||||
|
metadata.update(extra_meta)
|
||||||
|
|
||||||
|
return ServiceConfig(
|
||||||
|
service_name=service_name,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
ip=ip,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NacosManager:
|
||||||
|
"""Nacos 服务注册与心跳管理"""
|
||||||
|
|
||||||
|
def __init__(self, nacos_config: NacosConfig, service_config: ServiceConfig):
|
||||||
|
self.nacos_config = nacos_config
|
||||||
|
self.service_config = service_config
|
||||||
|
self.client = None
|
||||||
|
self._heartbeat_task: Optional[asyncio.Task] = None
|
||||||
|
self._stop_event = asyncio.Event()
|
||||||
|
self.is_registered = False
|
||||||
|
|
||||||
|
def _init_client(self) -> bool:
|
||||||
|
"""初始化 Nacos 客户端"""
|
||||||
|
if nacos is None:
|
||||||
|
raise ImportError("未安装 nacos-sdk-python,请先安装依赖")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client = nacos.NacosClient(
|
||||||
|
server_addresses=self.nacos_config.server_addresses,
|
||||||
|
namespace=self.nacos_config.namespace,
|
||||||
|
username=self.nacos_config.username,
|
||||||
|
password=self.nacos_config.password,
|
||||||
|
)
|
||||||
|
logger.info(f"Nacos 客户端初始化成功: {self.nacos_config.server_addresses}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Nacos 客户端初始化失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def register_service(self) -> bool:
|
||||||
|
"""注册服务到 Nacos"""
|
||||||
|
if not self.client and not self._init_client():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.add_naming_instance(
|
||||||
|
service_name=self.service_config.service_name,
|
||||||
|
ip=self.service_config.ip,
|
||||||
|
port=self.service_config.port,
|
||||||
|
cluster_name=self.nacos_config.cluster_name,
|
||||||
|
group_name=self.nacos_config.group_name,
|
||||||
|
weight=self.nacos_config.weight,
|
||||||
|
metadata=self.service_config.metadata,
|
||||||
|
ephemeral=self.nacos_config.ephemeral,
|
||||||
|
)
|
||||||
|
self.is_registered = True
|
||||||
|
logger.info(
|
||||||
|
"✅ 服务注册成功: %s (%s:%s)",
|
||||||
|
self.service_config.service_name,
|
||||||
|
self.service_config.ip,
|
||||||
|
self.service_config.port,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 服务注册失败: {e}")
|
||||||
|
self.is_registered = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def deregister_service(self) -> bool:
|
||||||
|
"""从 Nacos 注销服务"""
|
||||||
|
if not self.client or not self.is_registered:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.remove_naming_instance(
|
||||||
|
service_name=self.service_config.service_name,
|
||||||
|
ip=self.service_config.ip,
|
||||||
|
port=self.service_config.port,
|
||||||
|
cluster_name=self.nacos_config.cluster_name,
|
||||||
|
group_name=self.nacos_config.group_name,
|
||||||
|
)
|
||||||
|
self.is_registered = False
|
||||||
|
logger.info("✅ 服务注销成功: %s", self.service_config.service_name)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 服务注销失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _send_heartbeat(self) -> None:
|
||||||
|
"""发送心跳"""
|
||||||
|
if not self.client or not self.is_registered:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.client.send_heartbeat(
|
||||||
|
service_name=self.service_config.service_name,
|
||||||
|
ip=self.service_config.ip,
|
||||||
|
port=self.service_config.port,
|
||||||
|
cluster_name=self.nacos_config.cluster_name,
|
||||||
|
group_name=self.nacos_config.group_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _heartbeat_loop(self) -> None:
|
||||||
|
"""心跳循环"""
|
||||||
|
interval = max(1, self.nacos_config.heartbeat_interval)
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
self._send_heartbeat()
|
||||||
|
logger.debug("心跳发送成功: %s", self.service_config.service_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"心跳发送失败: {e}")
|
||||||
|
# 尝试重新注册
|
||||||
|
try:
|
||||||
|
self.register_service()
|
||||||
|
except Exception as re:
|
||||||
|
logger.error(f"重新注册失败: {re}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._stop_event.wait(), timeout=interval)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""启动注册与心跳"""
|
||||||
|
if not self.nacos_config.enabled:
|
||||||
|
logger.info("Nacos 未启用,跳过注册")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.register_service():
|
||||||
|
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
||||||
|
logger.info("✅ Nacos 心跳任务已启动")
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ Nacos 注册失败,服务继续运行")
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""停止心跳并注销"""
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
if self._heartbeat_task:
|
||||||
|
self._heartbeat_task.cancel()
|
||||||
|
try:
|
||||||
|
await self._heartbeat_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.deregister_service()
|
||||||
|
|
||||||
|
def status(self) -> Dict[str, Any]:
|
||||||
|
"""获取当前状态"""
|
||||||
|
return {
|
||||||
|
"service_name": self.service_config.service_name,
|
||||||
|
"ip": self.service_config.ip,
|
||||||
|
"port": self.service_config.port,
|
||||||
|
"namespace": self.nacos_config.namespace,
|
||||||
|
"group": self.nacos_config.group_name,
|
||||||
|
"cluster": self.nacos_config.cluster_name,
|
||||||
|
"registered": self.is_registered,
|
||||||
|
"heartbeat_running": self._heartbeat_task is not None and not self._heartbeat_task.done(),
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class PromptManager:
|
||||||
|
"""提示词配置管理器"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
self._config_path = config_path or os.path.join(root_dir, "config", "prompts.yaml")
|
||||||
|
self._data: Dict[str, Any] = {}
|
||||||
|
self.reload()
|
||||||
|
|
||||||
|
def reload(self) -> None:
|
||||||
|
"""重新加载提示词配置"""
|
||||||
|
with open(self._config_path, "r", encoding="utf-8") as f:
|
||||||
|
self._data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
def get(self, group: str, name: str, default: str = "") -> str:
|
||||||
|
"""获取指定提示词"""
|
||||||
|
return str(self._data.get(group, {}).get(name, default))
|
||||||
|
|
||||||
|
def list_groups(self) -> list[str]:
|
||||||
|
"""列出所有分组"""
|
||||||
|
return list(self._data.keys())
|
||||||
|
|
||||||
|
def list_prompts(self, group: str) -> list[str]:
|
||||||
|
"""列出分组内提示词"""
|
||||||
|
return list(self._data.get(group, {}).keys())
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class RagflowClient:
|
||||||
|
"""RAGFlow 客户端(仅检索)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
cfg = Config.get_section("ragflow")
|
||||||
|
self._base_url = cfg.get("url", "")
|
||||||
|
self._api_key = cfg.get("api_key", "")
|
||||||
|
self._retrieval_path = cfg.get("retrieval", "/api/v1/retrieval")
|
||||||
|
self._dataset_ids = cfg.get("dataset_ids", "")
|
||||||
|
|
||||||
|
def _build_url(self) -> str:
|
||||||
|
return self._base_url.rstrip("/") + "/" + self._retrieval_path.lstrip("/")
|
||||||
|
|
||||||
|
def retrieve(self, query: str, top_k: int = 3) -> Dict[str, Any]:
|
||||||
|
"""检索匹配文档"""
|
||||||
|
if not self._base_url or not self._retrieval_path:
|
||||||
|
raise RuntimeError("未配置 ragflow.url 或 ragflow.retrieval")
|
||||||
|
url = self._build_url()
|
||||||
|
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
|
||||||
|
payload = {
|
||||||
|
"dataset_ids": self._dataset_ids,
|
||||||
|
"query": query,
|
||||||
|
"top_k": top_k,
|
||||||
|
}
|
||||||
|
|
||||||
|
with httpx.Client(timeout=30) as client:
|
||||||
|
response = client.post(url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_table_name(record: Dict[str, Any]) -> Optional[str]:
|
||||||
|
"""从检索结果中提取表名"""
|
||||||
|
if not record:
|
||||||
|
return None
|
||||||
|
|
||||||
|
metadata = record.get("metadata") or {}
|
||||||
|
for key in ("table", "table_name"):
|
||||||
|
if key in metadata:
|
||||||
|
return metadata.get(key)
|
||||||
|
|
||||||
|
for key in ("table", "table_name"):
|
||||||
|
if key in record:
|
||||||
|
return record.get(key)
|
||||||
|
|
||||||
|
content = record.get("content") or record.get("text") or ""
|
||||||
|
for line in str(content).splitlines():
|
||||||
|
if line.lower().startswith("table:"):
|
||||||
|
return line.split(":", 1)[1].strip()
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from services.cache import NoopCache, RedisCache
|
||||||
|
from services.ragflow_client import RagflowClient, extract_table_name
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateMatcher:
|
||||||
|
"""模板匹配器:RAGFlow + Redis 缓存"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._ragflow = RagflowClient()
|
||||||
|
self._cache = self._init_cache()
|
||||||
|
cfg = Config.get_section("ragflow")
|
||||||
|
self._cache_ttl = int(cfg.get("cache_ttl", 600))
|
||||||
|
|
||||||
|
def _init_cache(self):
|
||||||
|
cfg = Config.get_section("redis")
|
||||||
|
enabled = str(cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
|
||||||
|
if not enabled:
|
||||||
|
return NoopCache()
|
||||||
|
|
||||||
|
url = cfg.get("url")
|
||||||
|
db = int(cfg.get("db", 0))
|
||||||
|
if not url:
|
||||||
|
return NoopCache()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return RedisCache(url=url, db=db)
|
||||||
|
except Exception:
|
||||||
|
return NoopCache()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cache_key(text: str) -> str:
|
||||||
|
return "ragflow:table:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def match(self, normalized_text: str) -> Dict[str, Any]:
|
||||||
|
"""返回匹配的表名与原始响应"""
|
||||||
|
key = self._cache_key(normalized_text)
|
||||||
|
cached = self._cache.get(key)
|
||||||
|
if cached:
|
||||||
|
return json.loads(cached)
|
||||||
|
try:
|
||||||
|
response = self._ragflow.retrieve(normalized_text, top_k=3)
|
||||||
|
except Exception as e:
|
||||||
|
result = {"table_name": None, "raw": {"error": str(e)}}
|
||||||
|
self._cache.set(key, json.dumps(result, ensure_ascii=False), self._cache_ttl)
|
||||||
|
return result
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if isinstance(data, list):
|
||||||
|
for item in data:
|
||||||
|
table_name = extract_table_name(item)
|
||||||
|
if table_name:
|
||||||
|
candidates.append(table_name)
|
||||||
|
|
||||||
|
matched = candidates[0] if candidates else None
|
||||||
|
result = {"table_name": matched, "raw": response}
|
||||||
|
|
||||||
|
self._cache.set(key, json.dumps(result, ensure_ascii=False), self._cache_ttl)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
from tools.calculator import CalculatorTool
|
||||||
|
from tools.web_search import WebSearchTool
|
||||||
|
from tools.rest_api_tool import RestApiTool
|
||||||
|
from tools.sr_api_tool import SrApiQueryTool
|
||||||
|
|
||||||
|
|
||||||
|
class ToolRouter:
|
||||||
|
"""工具路由器:统一调用入口"""
|
||||||
|
|
||||||
|
def __init__(self, tools: Optional[list[BaseTool]] = None):
|
||||||
|
if tools is None:
|
||||||
|
tools = [CalculatorTool(), WebSearchTool(), RestApiTool(), SrApiQueryTool()]
|
||||||
|
self._tools: Dict[str, BaseTool] = {tool.name: tool for tool in tools}
|
||||||
|
|
||||||
|
def list_tools(self) -> list[str]:
|
||||||
|
"""列出可用工具名称"""
|
||||||
|
return list(self._tools.keys())
|
||||||
|
|
||||||
|
def call(self, tool_name: str, payload: Any) -> Dict[str, Any]:
|
||||||
|
"""调用工具并返回标准化结果"""
|
||||||
|
tool = self._tools.get(tool_name)
|
||||||
|
if not tool:
|
||||||
|
return {"ok": False, "data": None, "error": f"工具不存在: {tool_name}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(payload, (dict, list)):
|
||||||
|
input_value = json.dumps(payload, ensure_ascii=False)
|
||||||
|
elif payload is None:
|
||||||
|
input_value = ""
|
||||||
|
else:
|
||||||
|
input_value = str(payload)
|
||||||
|
|
||||||
|
result = tool.run(input_value)
|
||||||
|
return {"ok": True, "data": result, "error": None}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "data": None, "error": str(e)}
|
||||||
+33
-17
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Basic tests for the LangChain + LangGraph scaffolding
|
LangChain + LangGraph 脚手架基础测试
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -8,18 +8,36 @@ import sys
|
|||||||
import os
|
import os
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'config.ini')
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
f.write("""
|
||||||
|
[General]
|
||||||
|
DEFAULT_MODEL_SECTION = gpt-4o
|
||||||
|
MAX_RETRIES = 1
|
||||||
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
[gpt-4o]
|
||||||
|
MODEL_NAME = gpt-4o
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
|
|
||||||
|
[gpt-3.5-turbo]
|
||||||
|
MODEL_NAME = gpt-3.5-turbo
|
||||||
|
OPENAI_API_KEY = your_openai_api_key_here
|
||||||
|
""")
|
||||||
|
|
||||||
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
from workflows.workflow_manager import WorkflowManager, WorkflowType
|
||||||
|
|
||||||
|
|
||||||
class TestWorkflowManager(unittest.TestCase):
|
class TestWorkflowManager(unittest.TestCase):
|
||||||
"""Test WorkflowManager functionality"""
|
"""测试 WorkflowManager 功能"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up test fixtures"""
|
"""设置测试夹具"""
|
||||||
self.manager = WorkflowManager()
|
self.manager = WorkflowManager()
|
||||||
|
|
||||||
def test_get_available_workflows(self):
|
def test_get_available_workflows(self):
|
||||||
"""Test that available workflows are returned"""
|
"""测试可用工作流返回"""
|
||||||
workflows = self.manager.get_available_workflows()
|
workflows = self.manager.get_available_workflows()
|
||||||
self.assertIsInstance(workflows, list)
|
self.assertIsInstance(workflows, list)
|
||||||
self.assertGreater(len(workflows), 0)
|
self.assertGreater(len(workflows), 0)
|
||||||
@@ -27,7 +45,7 @@ class TestWorkflowManager(unittest.TestCase):
|
|||||||
self.assertIn("tool_using", workflows)
|
self.assertIn("tool_using", workflows)
|
||||||
|
|
||||||
def test_get_workflow(self):
|
def test_get_workflow(self):
|
||||||
"""Test getting workflow instances"""
|
"""测试获取工作流实例"""
|
||||||
conversation_workflow = self.manager.get_workflow(WorkflowType.CONVERSATION)
|
conversation_workflow = self.manager.get_workflow(WorkflowType.CONVERSATION)
|
||||||
self.assertIsNotNone(conversation_workflow)
|
self.assertIsNotNone(conversation_workflow)
|
||||||
|
|
||||||
@@ -35,8 +53,8 @@ class TestWorkflowManager(unittest.TestCase):
|
|||||||
self.assertIsNotNone(tool_workflow)
|
self.assertIsNotNone(tool_workflow)
|
||||||
|
|
||||||
def test_session_management(self):
|
def test_session_management(self):
|
||||||
"""Test session creation and retrieval"""
|
"""测试会话创建与获取"""
|
||||||
# Execute a workflow to create a session
|
# 执行工作流以创建会话
|
||||||
result = self.manager.execute_workflow(
|
result = self.manager.execute_workflow(
|
||||||
WorkflowType.CONVERSATION,
|
WorkflowType.CONVERSATION,
|
||||||
"Hello, test session"
|
"Hello, test session"
|
||||||
@@ -45,23 +63,21 @@ class TestWorkflowManager(unittest.TestCase):
|
|||||||
session_id = result["session_id"]
|
session_id = result["session_id"]
|
||||||
self.assertIsNotNone(session_id)
|
self.assertIsNotNone(session_id)
|
||||||
|
|
||||||
# Test session info retrieval
|
# 测试会话信息获取
|
||||||
session_info = self.manager.get_session_info(session_id)
|
session_info = self.manager.get_session_info(session_id)
|
||||||
self.assertIsNotNone(session_info)
|
self.assertIsNotNone(session_info)
|
||||||
self.assertEqual(session_info["workflow_type"], WorkflowType.CONVERSATION)
|
self.assertEqual(session_info["workflow_type"], WorkflowType.CONVERSATION)
|
||||||
|
|
||||||
|
|
||||||
class TestConfiguration(unittest.TestCase):
|
class TestConfiguration(unittest.TestCase):
|
||||||
"""Test configuration validation"""
|
"""测试配置校验"""
|
||||||
|
|
||||||
def test_config_import(self):
|
def test_config_loading(self):
|
||||||
"""Test that configuration can be imported"""
|
"""测试能从 config.ini 加载配置"""
|
||||||
try:
|
from config import Config
|
||||||
from config import Config
|
model_config = Config.get_model_config()
|
||||||
# This should not raise an exception if .env file exists with valid API key
|
self.assertIn('model', model_config)
|
||||||
self.assertTrue(hasattr(Config, 'OPENAI_API_KEY'))
|
self.assertIn('api_key', model_config)
|
||||||
except ImportError:
|
|
||||||
self.fail("Could not import config module")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+5
-5
@@ -3,25 +3,25 @@ from langchain_core.tools import BaseTool
|
|||||||
|
|
||||||
|
|
||||||
class CalculatorTool(BaseTool):
|
class CalculatorTool(BaseTool):
|
||||||
"""A simple calculator tool for mathematical operations"""
|
"""用于数学运算的简单计算器工具"""
|
||||||
|
|
||||||
name: str = "calculator"
|
name: str = "calculator"
|
||||||
description: str = "Perform mathematical calculations. Input should be a mathematical expression like '2 + 2' or '10 * (3 + 5)'"
|
description: str = "Perform mathematical calculations. Input should be a mathematical expression like '2 + 2' or '10 * (3 + 5)'"
|
||||||
|
|
||||||
def _run(self, expression: str) -> str:
|
def _run(self, expression: str) -> str:
|
||||||
"""Evaluate a mathematical expression"""
|
"""计算数学表达式"""
|
||||||
try:
|
try:
|
||||||
# Security: Only allow safe mathematical operations
|
# 安全:仅允许安全的数学运算
|
||||||
allowed_chars = set("0123456789+-*/(). ")
|
allowed_chars = set("0123456789+-*/(). ")
|
||||||
if not all(c in allowed_chars for c in expression):
|
if not all(c in allowed_chars for c in expression):
|
||||||
return "Error: Expression contains invalid characters"
|
return "Error: Expression contains invalid characters"
|
||||||
|
|
||||||
# Evaluate the expression
|
# 计算表达式
|
||||||
result = eval(expression)
|
result = eval(expression)
|
||||||
return f"Result: {result}"
|
return f"Result: {result}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error calculating expression: {str(e)}"
|
return f"Error calculating expression: {str(e)}"
|
||||||
|
|
||||||
async def _arun(self, expression: str) -> str:
|
async def _arun(self, expression: str) -> str:
|
||||||
"""Async version of the tool"""
|
"""工具的异步版本"""
|
||||||
return self._run(expression)
|
return self._run(expression)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
|
||||||
|
class RestApiTool(BaseTool):
|
||||||
|
"""RESTful API 调用工具"""
|
||||||
|
|
||||||
|
name: str = "rest_api"
|
||||||
|
description: str = (
|
||||||
|
"调用 RESTful API。输入为 JSON 字符串,示例:"
|
||||||
|
'{"method":"GET","url":"https://example.com/api","params":{},"headers":{},"body":null,"timeout":30}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run(self, payload: str) -> str:
|
||||||
|
"""执行 RESTful API 请求"""
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求参数解析失败: {e}"
|
||||||
|
|
||||||
|
method = str(data.get("method", "GET")).upper()
|
||||||
|
url = data.get("url")
|
||||||
|
params: Dict[str, Any] = data.get("params") or {}
|
||||||
|
headers: Dict[str, Any] = data.get("headers") or {}
|
||||||
|
body = data.get("body")
|
||||||
|
timeout = float(data.get("timeout", 30))
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return "缺少 url"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=timeout) as client:
|
||||||
|
response = client.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"headers": dict(response.headers),
|
||||||
|
"text": response.text,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求失败: {e}"
|
||||||
|
|
||||||
|
async def _arun(self, payload: str) -> str:
|
||||||
|
"""工具的异步版本"""
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求参数解析失败: {e}"
|
||||||
|
|
||||||
|
method = str(data.get("method", "GET")).upper()
|
||||||
|
url = data.get("url")
|
||||||
|
params: Dict[str, Any] = data.get("params") or {}
|
||||||
|
headers: Dict[str, Any] = data.get("headers") or {}
|
||||||
|
body = data.get("body")
|
||||||
|
timeout = float(data.get("timeout", 30))
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return "缺少 url"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
response = await client.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"headers": dict(response.headers),
|
||||||
|
"text": response.text,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求失败: {e}"
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class SrApiQueryTool(BaseTool):
|
||||||
|
"""调用 SR API 执行 SQL 查询"""
|
||||||
|
|
||||||
|
name: str = "sr_api_query"
|
||||||
|
description: str = (
|
||||||
|
"调用 SR API 执行 SQL 查询。输入为 JSON 字符串,示例:"
|
||||||
|
'{"sql":"SELECT * FROM table","page":1,"rows":10,"orderBySelect":true,"timeout":30}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run(self, payload: str) -> str:
|
||||||
|
"""执行 SQL 查询"""
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求参数解析失败: {e}"
|
||||||
|
|
||||||
|
sql = data.get("sql")
|
||||||
|
page = int(data.get("page", 1))
|
||||||
|
rows = int(data.get("rows", 10))
|
||||||
|
order_by_select = bool(data.get("orderBySelect", True))
|
||||||
|
timeout = float(data.get("timeout", 30))
|
||||||
|
|
||||||
|
if not sql:
|
||||||
|
return "缺少 sql"
|
||||||
|
|
||||||
|
cfg = Config.get_section("sr_api")
|
||||||
|
url = cfg.get("url")
|
||||||
|
app_key = cfg.get("llzappkey")
|
||||||
|
secret_key = cfg.get("llzsercret")
|
||||||
|
|
||||||
|
if not url or not app_key or not secret_key:
|
||||||
|
return "sr_api 配置缺失 url/llzAppkey/llzSercret"
|
||||||
|
|
||||||
|
headers = {"llzAppkey": app_key, "llzSercret": secret_key}
|
||||||
|
body = {"sql": sql, "page": page, "rows": rows, "orderBySelect": order_by_select}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=timeout) as client:
|
||||||
|
response = client.post(url, json=body, headers=headers)
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"headers": dict(response.headers),
|
||||||
|
"text": response.text,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求失败: {e}"
|
||||||
|
|
||||||
|
async def _arun(self, payload: str) -> str:
|
||||||
|
"""工具的异步版本"""
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求参数解析失败: {e}"
|
||||||
|
|
||||||
|
sql = data.get("sql")
|
||||||
|
page = int(data.get("page", 1))
|
||||||
|
rows = int(data.get("rows", 10))
|
||||||
|
order_by_select = bool(data.get("orderBySelect", True))
|
||||||
|
timeout = float(data.get("timeout", 30))
|
||||||
|
|
||||||
|
if not sql:
|
||||||
|
return "缺少 sql"
|
||||||
|
|
||||||
|
cfg = Config.get_section("sr_api")
|
||||||
|
url = cfg.get("url")
|
||||||
|
app_key = cfg.get("llzappkey")
|
||||||
|
secret_key = cfg.get("llzsercret")
|
||||||
|
|
||||||
|
if not url or not app_key or not secret_key:
|
||||||
|
return "sr_api 配置缺失 url/llzAppkey/llzSercret"
|
||||||
|
|
||||||
|
headers = {"llzAppkey": app_key, "llzSercret": secret_key}
|
||||||
|
body = {"sql": sql, "page": page, "rows": rows, "orderBySelect": order_by_select}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
response = await client.post(url, json=body, headers=headers)
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"headers": dict(response.headers),
|
||||||
|
"text": response.text,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"请求失败: {e}"
|
||||||
+9
-9
@@ -4,22 +4,22 @@ import requests
|
|||||||
|
|
||||||
|
|
||||||
class WebSearchTool(BaseTool):
|
class WebSearchTool(BaseTool):
|
||||||
"""A tool for searching the web (placeholder implementation)"""
|
"""网络搜索工具(占位实现)"""
|
||||||
|
|
||||||
name: str = "web_search"
|
name: str = "web_search"
|
||||||
description: str = "Search the web for information. Input should be a search query."
|
description: str = "Search the web for information. Input should be a search query."
|
||||||
|
|
||||||
def _run(self, query: str) -> str:
|
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
|
# 真实实现需接入搜索 API
|
||||||
# like Serper, Tavily, or Google Search API
|
# 如 Serper、Tavily 或 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:
|
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)
|
1. Sign up for a search API service (e.g., Serper, Tavily)
|
||||||
2. Add your API key to the .env file
|
2. Add your API key to the config/config.ini file
|
||||||
3. Implement the actual search logic here"
|
3. Implement the actual search logic here"""
|
||||||
|
|
||||||
async def _arun(self, query: str) -> str:
|
async def _arun(self, query: str) -> str:
|
||||||
"""Async version of the tool"""
|
"""工具的异步版本"""
|
||||||
return self._run(query)
|
return self._run(query)
|
||||||
@@ -1,45 +1,45 @@
|
|||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from agents.conversation_agent import ConversationAgent
|
from agent.conversation import ConversationAgent
|
||||||
from agents.tool_agent import ToolAgent
|
from agent.tool import ToolAgent
|
||||||
|
|
||||||
|
|
||||||
class WorkflowType(Enum):
|
class WorkflowType(Enum):
|
||||||
"""Available workflow types"""
|
"""可用的工作流类型"""
|
||||||
CONVERSATION = "conversation"
|
CONVERSATION = "conversation"
|
||||||
TOOL_USING = "tool_using"
|
TOOL_USING = "tool_using"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowManager:
|
class WorkflowManager:
|
||||||
"""Manages different workflow types and their execution"""
|
"""管理不同工作流类型及其执行"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, default_model_section: Optional[str] = None):
|
||||||
self.workflows = {
|
self.workflows = {
|
||||||
WorkflowType.CONVERSATION: ConversationAgent(),
|
WorkflowType.CONVERSATION: ConversationAgent(model_section=default_model_section),
|
||||||
WorkflowType.TOOL_USING: ToolAgent()
|
WorkflowType.TOOL_USING: ToolAgent(model_section=default_model_section)
|
||||||
}
|
}
|
||||||
self.active_sessions: Dict[str, Any] = {}
|
self.active_sessions: Dict[str, Any] = {}
|
||||||
|
|
||||||
def get_workflow(self, workflow_type: WorkflowType):
|
def get_workflow(self, workflow_type: WorkflowType):
|
||||||
"""Get a workflow instance"""
|
"""获取工作流实例"""
|
||||||
return self.workflows.get(workflow_type)
|
return self.workflows.get(workflow_type)
|
||||||
|
|
||||||
def execute_workflow(self, workflow_type: WorkflowType, user_input: str,
|
def execute_workflow(self, workflow_type: WorkflowType, user_input: str,
|
||||||
session_id: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
session_id: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||||
"""Execute a specific workflow"""
|
"""执行指定工作流"""
|
||||||
workflow = self.get_workflow(workflow_type)
|
workflow = self.get_workflow(workflow_type)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
return {"error": f"Workflow {workflow_type.value} not found"}
|
return {"error": f"Workflow {workflow_type.value} not found"}
|
||||||
|
|
||||||
# Generate session ID if not provided
|
# 未提供会话 ID 时生成
|
||||||
if not session_id:
|
if not session_id:
|
||||||
session_id = f"session_{len(self.active_sessions) + 1}"
|
session_id = f"session_{len(self.active_sessions) + 1}"
|
||||||
|
|
||||||
# Execute the workflow
|
# 执行工作流
|
||||||
result = workflow.run(user_input, **kwargs)
|
result = workflow.run(user_input, **kwargs)
|
||||||
|
|
||||||
# Store session data
|
# 存储会话数据
|
||||||
self.active_sessions[session_id] = {
|
self.active_sessions[session_id] = {
|
||||||
"workflow_type": workflow_type,
|
"workflow_type": workflow_type,
|
||||||
"last_result": result,
|
"last_result": result,
|
||||||
@@ -53,20 +53,20 @@ class WorkflowManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_available_workflows(self) -> List[str]:
|
def get_available_workflows(self) -> List[str]:
|
||||||
"""Get list of available workflow types"""
|
"""获取可用工作流列表"""
|
||||||
return [workflow.value for workflow in WorkflowType]
|
return [workflow.value for workflow in WorkflowType]
|
||||||
|
|
||||||
def _get_timestamp(self) -> str:
|
def _get_timestamp(self) -> str:
|
||||||
"""Get current timestamp"""
|
"""获取当前时间戳"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
return datetime.now().isoformat()
|
return datetime.now().isoformat()
|
||||||
|
|
||||||
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Get information about a session"""
|
"""获取会话信息"""
|
||||||
return self.active_sessions.get(session_id)
|
return self.active_sessions.get(session_id)
|
||||||
|
|
||||||
def cleanup_sessions(self, older_than_hours: int = 24):
|
def cleanup_sessions(self, older_than_hours: int = 24):
|
||||||
"""Clean up old sessions"""
|
"""清理过期会话"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
|
cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
|
||||||
|
|||||||
Reference in New Issue
Block a user