in devel
This commit is contained in:
parent
4ab494aae5
commit
169666443f
|
|
@ -1,160 +1,386 @@
|
||||||
"""客户端:用户交互 & 会话管理"""
|
|
||||||
"""
|
"""
|
||||||
client/agent_client.py
|
client/agent_client.py
|
||||||
Agent 客户端:协调 LLM 引擎、MCP 服务器、记忆模块,驱动完整 Agent 执行流程
|
Agent 客户端:驱动完整 OpenAI Function Calling + Tool Chain 执行流程
|
||||||
|
新增: OpenAI 格式消息序列管理,支持多轮工具调用上下文传递
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
import uuid
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from config.settings import settings
|
||||||
from llm.llm_engine import LLMEngine
|
from llm.llm_engine import LLMEngine
|
||||||
from mcp.mcp_protocol import MCPMethod, MCPRequest
|
from mcp.mcp_protocol import (
|
||||||
|
ChainPlan, ChainResult, MCPRequest, MCPResponse,
|
||||||
|
StepResult, ToolStep,
|
||||||
|
)
|
||||||
from mcp.mcp_server import MCPServer
|
from mcp.mcp_server import MCPServer
|
||||||
from memory.memory_store import MemoryStore
|
from memory.memory_store import MemoryStore
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
# ── 单轮执行结果 ───────────────────────────────────────────────
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentResponse:
|
class AgentResponse:
|
||||||
"""一次完整 Agent 调用的结果"""
|
"""一次完整 Agent 调用的结果"""
|
||||||
user_input: str
|
user_input: str
|
||||||
final_reply: str
|
final_reply: str
|
||||||
tool_used: str | None = None
|
chain_result: ChainResult | None = None
|
||||||
tool_output: str | None = None
|
tool_used: str | None = None
|
||||||
success: bool = True
|
tool_output: str | None = None
|
||||||
error: str | None = None
|
success: bool = True
|
||||||
|
error: str | None = None
|
||||||
|
token_usage: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_multi_step(self) -> bool:
|
||||||
|
return self.chain_result is not None and self.chain_result.total_steps > 1
|
||||||
|
|
||||||
|
|
||||||
# ── Agent 客户端 ───────────────────────────────────────────────
|
|
||||||
class AgentClient:
|
class AgentClient:
|
||||||
"""
|
"""
|
||||||
Agent 客户端:实现完整的 ReAct 执行循环
|
Agent 客户端:OpenAI Function Calling + Multi-Step Tool Chain
|
||||||
|
|
||||||
执行流程 (5步):
|
执行流程:
|
||||||
1. [CLIENT] 接收用户输入,写入 Memory
|
1. [CLIENT] 接收用户输入,写入 Memory
|
||||||
2. [LLM] 分析意图,决策是否调用工具
|
2. [LLM] plan_tool_chain() → OpenAI Function Calling → ChainPlan
|
||||||
3. [MCP] 构造 JSON-RPC 请求,发送给 MCP Server
|
3. [CHAIN] 串行执行每个 ToolStep:
|
||||||
4. [TOOL] MCP Server 执行工具,返回结果
|
a. 构造 MCPRequest → MCPServer 执行工具
|
||||||
5. [LLM] 整合结果,生成最终回复,写入 Memory
|
b. 将工具结果追加到 OpenAI 消息序列(tool role)
|
||||||
|
c. 记录 StepResult,更新链路上下文(占位符替换)
|
||||||
|
4. [LLM] generate_chain_reply() → OpenAI 整合结果 → 最终回复
|
||||||
|
5. [MEMORY] 写入完整调用链记录
|
||||||
|
|
||||||
使用示例:
|
OpenAI 消息序列示例(多步骤):
|
||||||
client = AgentClient(llm=llm, mcp_server=mcp, memory=memory)
|
{"role": "system", "content": "规划器提示"}
|
||||||
response = client.chat("帮我计算 100 * 200")
|
{"role": "user", "content": "搜索天气然后计算..."}
|
||||||
print(response.final_reply)
|
{"role": "assistant", "tool_calls": [{web_search}, {calculator}]}
|
||||||
|
{"role": "tool", "content": "web_search 结果", "tool_call_id": "call_1"}
|
||||||
|
{"role": "tool", "content": "calculator 结果", "tool_call_id": "call_2"}
|
||||||
|
→ generate_reply() → 最终自然语言回复
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
llm: LLMEngine,
|
llm: LLMEngine,
|
||||||
mcp_server: MCPServer,
|
mcp_server: MCPServer,
|
||||||
memory: MemoryStore,
|
memory: MemoryStore,
|
||||||
prompt: str = ""
|
|
||||||
):
|
):
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
self.mcp_server = mcp_server
|
self.mcp_server = mcp_server
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.agent_prompt = prompt
|
self.logger = get_logger("CLIENT")
|
||||||
self.logger = get_logger("CLIENT")
|
# OpenAI 格式的结构化对话历史(跨轮次保持上下文)
|
||||||
self.logger.info("💻 Agent Client 初始化完成")
|
self._openai_history: list[dict] = []
|
||||||
|
self.logger.info("💻 Agent Client 初始化完成(OpenAI Function Calling 模式)")
|
||||||
|
|
||||||
# ── 主入口 ──────────────────────────────────────────────────
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 主入口
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def chat(self, user_input: str) -> AgentResponse:
|
def chat(self, user_input: str) -> AgentResponse:
|
||||||
"""
|
"""处理一轮用户对话"""
|
||||||
处理一轮用户对话,执行完整 Agent 流程
|
sep = "═" * 60
|
||||||
|
self.logger.info(sep)
|
||||||
|
self.logger.info(f"📨 收到用户输入: {user_input}")
|
||||||
|
self.logger.info(sep)
|
||||||
|
|
||||||
Args:
|
# Step 1: 记录用户消息
|
||||||
user_input: 用户输入的自然语言文本
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AgentResponse 实例
|
|
||||||
"""
|
|
||||||
self.logger.info(f"{'=' * 55}")
|
|
||||||
self.logger.info(f"📨 Step 1 [CLIENT] 收到用户输入: {user_input}")
|
|
||||||
|
|
||||||
# ── Step 1: 记录用户消息 ────────────────────────────────
|
|
||||||
self.memory.add_user_message(user_input)
|
self.memory.add_user_message(user_input)
|
||||||
context = self.memory.get_context_summary()
|
context = self.memory.get_context_summary()
|
||||||
|
|
||||||
# ── Step 2: LLM 推理决策 ────────────────────────────────
|
# Step 2: LLM 规划工具调用链
|
||||||
self.logger.info("🧠 Step 2 [LLM] 开始推理,分析意图...")
|
self.logger.info("🗺 Step 2 [LLM] 规划工具调用链...")
|
||||||
tool_schemas = self.mcp_server.get_tool_schemas()
|
tool_schemas = self.mcp_server.get_tool_schemas()
|
||||||
decision = self.llm.think_and_decide(user_input, tool_schemas, context, self.agent_prompt)
|
plan: ChainPlan = self.llm.plan_tool_chain(
|
||||||
|
user_input=user_input,
|
||||||
|
tool_schemas=tool_schemas,
|
||||||
|
context=context,
|
||||||
|
history=self._openai_history,
|
||||||
|
)
|
||||||
|
|
||||||
# ── 分支:是否需要工具 ──────────────────────────────────
|
# 无需工具:直接回复
|
||||||
if not decision.need_tool:
|
if not plan.steps:
|
||||||
return self._handle_direct_reply(user_input, context)
|
return self._handle_direct_reply(user_input, context)
|
||||||
|
|
||||||
return self._handle_tool_call(user_input, decision, context)
|
# Step 3~4: 执行工具调用链,构造 OpenAI 消息序列
|
||||||
|
chain_result, tool_messages = self._execute_chain(plan, user_input)
|
||||||
|
|
||||||
# ── 私有流程方法 ────────────────────────────────────────────
|
# Step 5: 调用 OpenAI 整合结果,生成最终回复
|
||||||
|
return self._generate_response(user_input, chain_result, tool_messages, context)
|
||||||
|
|
||||||
def _handle_direct_reply(self, user_input: str, context: str) -> AgentResponse:
|
# ════════════════════════════════════════════════════════════
|
||||||
"""无需工具时直接生成回复"""
|
# 串行执行引擎
|
||||||
self.logger.info("💬 无需工具,直接生成回复")
|
# ════════════════════════════════════════════════════════════
|
||||||
reply = self.llm.generate_direct_reply(user_input, context)
|
|
||||||
self.memory.add_assistant_message(reply)
|
|
||||||
return AgentResponse(user_input=user_input, final_reply=reply)
|
|
||||||
|
|
||||||
def _handle_tool_call(
|
def _execute_chain(
|
||||||
self,
|
self,
|
||||||
user_input: str,
|
plan: ChainPlan,
|
||||||
decision,
|
user_input: str,
|
||||||
context: str,
|
) -> tuple[ChainResult, list[dict]]:
|
||||||
) -> AgentResponse:
|
"""
|
||||||
"""执行工具调用的完整流程(Step 3 → 4 → 5)"""
|
串行执行工具调用链,同步构造 OpenAI 消息序列
|
||||||
|
|
||||||
# ── Step 3: 构造 MCP 请求 ───────────────────────────────
|
Returns:
|
||||||
mcp_request: MCPRequest = decision.to_mcp_request()
|
(ChainResult, tool_messages)
|
||||||
|
tool_messages 为 OpenAI 格式的工具调用消息列表,
|
||||||
|
用于后续 generate_reply() 调用
|
||||||
|
"""
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"📡 Step 3 [MCP] 发送工具调用请求\n"
|
f"\n{'─' * 60}\n"
|
||||||
f" 方法: {mcp_request.method}\n"
|
f" 🔗 开始执行工具调用链\n"
|
||||||
f" 工具: {decision.tool_name}\n"
|
f" 目标: {plan.goal}\n"
|
||||||
f" 参数: {decision.arguments}\n"
|
f" 步骤: {plan.step_count} 步\n"
|
||||||
f" 请求体: {mcp_request.to_dict()}"
|
f"{'─' * 60}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Step 4: MCP Server 执行工具 ─────────────────────────
|
step_results: list[StepResult] = []
|
||||||
self.logger.info(f"🔧 Step 4 [TOOL] MCP Server 执行工具 [{decision.tool_name}]...")
|
chain_context: dict[str, str] = {}
|
||||||
mcp_response = self.mcp_server.handle_request(mcp_request)
|
tool_messages: list[dict] = []
|
||||||
|
failed_step: int | None = None
|
||||||
|
|
||||||
if not mcp_response.success:
|
# 构造 assistant 消息(含 tool_calls 声明)
|
||||||
error_msg = f"工具调用失败: {mcp_response.error}"
|
assistant_tool_calls = self._build_assistant_tool_calls(plan)
|
||||||
self.logger.error(f"❌ {error_msg}")
|
tool_messages.append({
|
||||||
return AgentResponse(
|
"role": "assistant",
|
||||||
user_input=user_input,
|
"content": None,
|
||||||
final_reply=f"抱歉,工具调用失败:{mcp_response.error.get('message')}",
|
"tool_calls": assistant_tool_calls,
|
||||||
tool_used=decision.tool_name,
|
})
|
||||||
|
|
||||||
|
for step in plan.steps:
|
||||||
|
# 检查前置依赖
|
||||||
|
if self._has_failed_dependency(step, failed_step):
|
||||||
|
self.logger.warning(
|
||||||
|
f"⏭ Step {step.step_id} [{step.tool_name}] 跳过"
|
||||||
|
f"(依赖步骤 {failed_step} 失败)"
|
||||||
|
)
|
||||||
|
step_results.append(StepResult(
|
||||||
|
step_id=step.step_id,
|
||||||
|
tool_name=step.tool_name,
|
||||||
|
success=False,
|
||||||
|
output="",
|
||||||
|
error=f"跳过:依赖步骤 {failed_step} 失败",
|
||||||
|
))
|
||||||
|
# 向 OpenAI 消息序列写入失败占位
|
||||||
|
tool_messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"content": f"步骤跳过:依赖步骤 {failed_step} 执行失败",
|
||||||
|
"tool_call_id": assistant_tool_calls[step.step_id - 1]["id"],
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 执行单步
|
||||||
|
result, tool_call_id = self._execute_single_step(
|
||||||
|
step, chain_context, assistant_tool_calls
|
||||||
|
)
|
||||||
|
step_results.append(result)
|
||||||
|
|
||||||
|
# 追加 tool 消息到 OpenAI 序列
|
||||||
|
tool_messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"content": result.output if result.success else f"执行失败: {result.error}",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
chain_context[result.context_key] = result.output
|
||||||
|
self.memory.add_tool_result(step.tool_name, result.output)
|
||||||
|
else:
|
||||||
|
failed_step = step.step_id
|
||||||
|
|
||||||
|
overall_success = failed_step is None
|
||||||
|
chain_result = ChainResult(
|
||||||
|
goal=plan.goal,
|
||||||
|
step_results=step_results,
|
||||||
|
success=overall_success,
|
||||||
|
failed_step=failed_step,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"{'─' * 60}\n"
|
||||||
|
f" {'✅ 调用链执行完成' if overall_success else '⚠️ 调用链部分失败'}\n"
|
||||||
|
f" 完成: {chain_result.completed_steps}/{chain_result.total_steps} 步\n"
|
||||||
|
f"{'─' * 60}"
|
||||||
|
)
|
||||||
|
return chain_result, tool_messages
|
||||||
|
|
||||||
|
def _execute_single_step(
|
||||||
|
self,
|
||||||
|
step: ToolStep,
|
||||||
|
chain_context: dict[str, str],
|
||||||
|
assistant_tool_calls: list[dict],
|
||||||
|
) -> tuple[StepResult, str]:
|
||||||
|
"""
|
||||||
|
执行单个步骤,返回 (StepResult, tool_call_id)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StepResult: 步骤执行结果
|
||||||
|
tool_call_id: 对应的 OpenAI tool_call_id(用于消息序列关联)
|
||||||
|
"""
|
||||||
|
# 注入前步上下文(占位符替换)
|
||||||
|
resolved_step = step.inject_context(chain_context)
|
||||||
|
tool_call_id = assistant_tool_calls[step.step_id - 1]["id"]
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"\n ▶ Step {step.step_id} 执行中\n"
|
||||||
|
f" 工具: [{resolved_step.tool_name}]\n"
|
||||||
|
f" 说明: {resolved_step.description}\n"
|
||||||
|
f" 参数: {resolved_step.arguments}\n"
|
||||||
|
f" call_id: {tool_call_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构造并发送 MCP 请求
|
||||||
|
mcp_request: MCPRequest = resolved_step.to_mcp_request()
|
||||||
|
mcp_response: MCPResponse = self.mcp_server.handle_request(mcp_request)
|
||||||
|
|
||||||
|
if mcp_response.success:
|
||||||
|
output = mcp_response.content
|
||||||
|
self.logger.info(f" ✅ Step {step.step_id} 成功: {output[:80]}...")
|
||||||
|
return StepResult(
|
||||||
|
step_id=step.step_id,
|
||||||
|
tool_name=step.tool_name,
|
||||||
|
success=True,
|
||||||
|
output=output,
|
||||||
|
), tool_call_id
|
||||||
|
else:
|
||||||
|
error_msg = mcp_response.error.get("message", "未知错误")
|
||||||
|
self.logger.error(f" ❌ Step {step.step_id} 失败: {error_msg}")
|
||||||
|
return StepResult(
|
||||||
|
step_id=step.step_id,
|
||||||
|
tool_name=step.tool_name,
|
||||||
success=False,
|
success=False,
|
||||||
|
output="",
|
||||||
error=error_msg,
|
error=error_msg,
|
||||||
|
), tool_call_id
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 回复生成
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _generate_response(
|
||||||
|
self,
|
||||||
|
user_input: str,
|
||||||
|
chain_result: ChainResult,
|
||||||
|
tool_messages: list[dict],
|
||||||
|
context: str,
|
||||||
|
) -> AgentResponse:
|
||||||
|
"""调用 OpenAI 整合工具结果,生成最终 AgentResponse"""
|
||||||
|
self.logger.info("✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...")
|
||||||
|
|
||||||
|
chain_summary = self._build_chain_summary(chain_result)
|
||||||
|
|
||||||
|
# 单步走简洁路径
|
||||||
|
if chain_result.total_steps == 1:
|
||||||
|
r = chain_result.step_results[0]
|
||||||
|
final_reply = self.llm.generate_final_reply(
|
||||||
|
user_input=user_input,
|
||||||
|
tool_name=r.tool_name,
|
||||||
|
tool_output=r.output,
|
||||||
|
context=context,
|
||||||
|
tool_call_id=tool_messages[-1].get("tool_call_id", "") if tool_messages else "",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 多步走完整 OpenAI 消息序列路径
|
||||||
|
final_reply = self.llm.generate_chain_reply(
|
||||||
|
user_input=user_input,
|
||||||
|
chain_summary=chain_summary,
|
||||||
|
context=context,
|
||||||
|
tool_messages=tool_messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
tool_output = mcp_response.content
|
chain_result.final_reply = final_reply
|
||||||
self.logger.info(f"✅ 工具执行成功,输出: {tool_output[:80]}...")
|
|
||||||
self.memory.add_tool_result(decision.tool_name, tool_output)
|
|
||||||
|
|
||||||
# ── Step 5: LLM 整合结果,生成最终回复 ──────────────────
|
# 更新 OpenAI 结构化历史(供下一轮使用)
|
||||||
self.logger.info("✍️ Step 5 [LLM] 整合工具结果,生成最终回复...")
|
self._openai_history.append({"role": "user", "content": user_input})
|
||||||
final_reply = self.llm.generate_final_reply(
|
self._openai_history.append({"role": "assistant", "content": final_reply})
|
||||||
user_input, decision.tool_name, tool_output, context
|
# 保留最近 10 轮
|
||||||
)
|
if len(self._openai_history) > 20:
|
||||||
self.memory.add_assistant_message(final_reply)
|
self._openai_history = self._openai_history[-20:]
|
||||||
|
|
||||||
self.logger.info(f"🎉 [CLIENT] 流程完成,回复已返回")
|
# 写入 Memory
|
||||||
|
if chain_result.total_steps > 1:
|
||||||
|
self.memory.add_chain_result(chain_result)
|
||||||
|
else:
|
||||||
|
self.memory.add_assistant_message(final_reply)
|
||||||
|
|
||||||
|
self.logger.info("🎉 [CLIENT] 流程完成,回复已返回")
|
||||||
return AgentResponse(
|
return AgentResponse(
|
||||||
user_input=user_input,
|
user_input=user_input,
|
||||||
final_reply=final_reply,
|
final_reply=final_reply,
|
||||||
tool_used=decision.tool_name,
|
chain_result=chain_result,
|
||||||
tool_output=tool_output,
|
tool_used=chain_result.step_results[0].tool_name if chain_result.total_steps == 1 else None,
|
||||||
|
tool_output=chain_result.step_results[0].output if chain_result.total_steps == 1 else None,
|
||||||
|
success=chain_result.success,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 工具方法 ────────────────────────────────────────────────
|
def _handle_direct_reply(self, user_input: str, context: str) -> AgentResponse:
|
||||||
|
"""无需工具时直接调用 OpenAI 生成回复"""
|
||||||
|
self.logger.info("💬 无需工具,直接调用 OpenAI 生成回复")
|
||||||
|
reply = self.llm.generate_direct_reply(user_input, context)
|
||||||
|
self.memory.add_assistant_message(reply)
|
||||||
|
self._openai_history.append({"role": "user", "content": user_input})
|
||||||
|
self._openai_history.append({"role": "assistant", "content": reply})
|
||||||
|
return AgentResponse(user_input=user_input, final_reply=reply)
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 工具方法
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_assistant_tool_calls(plan: ChainPlan) -> list[dict]:
|
||||||
|
"""
|
||||||
|
构造 OpenAI assistant 消息中的 tool_calls 字段
|
||||||
|
|
||||||
|
格式:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "calculator",
|
||||||
|
"arguments": '{"expression": "1+2"}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
tool_calls = []
|
||||||
|
for step in plan.steps:
|
||||||
|
tool_calls.append({
|
||||||
|
"id": f"call_{uuid.uuid4().hex[:8]}",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": step.tool_name,
|
||||||
|
"arguments": json.dumps(step.arguments, ensure_ascii=False),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return tool_calls
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_chain_summary(chain_result: ChainResult) -> str:
|
||||||
|
"""将调用链结果格式化为 LLM 可读的摘要"""
|
||||||
|
lines = []
|
||||||
|
for r in chain_result.step_results:
|
||||||
|
if r.success:
|
||||||
|
lines.append(
|
||||||
|
f"**Step {r.step_id} [{r.tool_name}]** ✅\n"
|
||||||
|
f"```\n{r.output[:300]}\n```"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"**Step {r.step_id} [{r.tool_name}]** ❌\n"
|
||||||
|
f"错误: {r.error}"
|
||||||
|
)
|
||||||
|
return "\n\n".join(lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_failed_dependency(step: ToolStep, failed_step: int | None) -> bool:
|
||||||
|
return failed_step is not None and failed_step in step.depends_on
|
||||||
|
|
||||||
def get_memory_stats(self) -> dict:
|
def get_memory_stats(self) -> dict:
|
||||||
"""获取当前记忆统计"""
|
stats = self.memory.stats()
|
||||||
return self.memory.stats()
|
stats["openai_history_len"] = len(self._openai_history)
|
||||||
|
return stats
|
||||||
|
|
||||||
def clear_session(self) -> None:
|
def clear_session(self) -> None:
|
||||||
"""清空当前会话"""
|
|
||||||
self.memory.clear_history()
|
self.memory.clear_history()
|
||||||
self.logger.info("🗑 会话已清空")
|
self._openai_history.clear()
|
||||||
|
self.logger.info("🗑 会话已清空(含 OpenAI 消息历史)")
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# config/config.yaml
|
||||||
|
# Agent 系统全局配置文件
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── LLM 模型配置 ───────────────────────────────────────────────
|
||||||
|
llm:
|
||||||
|
provider: "openai" # 模型提供商: openai | anthropic | ollama | local
|
||||||
|
model_name: "gpt-4o" # 模型名称
|
||||||
|
api_key: "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR" # API Key(优先读取环境变量 LLM_API_KEY)
|
||||||
|
api_base_url: "https://openapi.monica.im/v1" # 自定义 API 地址(兼容第三方 OpenAI 代理)
|
||||||
|
max_tokens: 4096 # 最大输出 Token 数
|
||||||
|
temperature: 0.7 # 生成温度 0.0~1.0
|
||||||
|
timeout: 60 # 请求超时(秒)
|
||||||
|
max_retries: 3 # 失败自动重试次数
|
||||||
|
# OpenAI 专用
|
||||||
|
function_calling: true # 是否启用 Function Calling(工具规划核心)
|
||||||
|
stream: false # 是否启用流式输出
|
||||||
|
# Ollama / 本地模型专用
|
||||||
|
model_path: "" # 本地模型路径,例如 /models/llama3
|
||||||
|
ollama_host: "http://localhost:11434"
|
||||||
|
|
||||||
|
# ── MCP Server 配置 ────────────────────────────────────────────
|
||||||
|
mcp:
|
||||||
|
server_name: "DemoMCPServer"
|
||||||
|
transport: "stdio"
|
||||||
|
host: "localhost"
|
||||||
|
port: 3000
|
||||||
|
enabled_tools:
|
||||||
|
- calculator
|
||||||
|
- web_search
|
||||||
|
- file_reader
|
||||||
|
- code_executor
|
||||||
|
|
||||||
|
# ── 工具配置 ───────────────────────────────────────────────────
|
||||||
|
tools:
|
||||||
|
web_search:
|
||||||
|
max_results: 5
|
||||||
|
timeout: 10
|
||||||
|
api_key: ""
|
||||||
|
engine: "mock"
|
||||||
|
|
||||||
|
file_reader:
|
||||||
|
allowed_root: "./workspace"
|
||||||
|
max_file_size_kb: 512
|
||||||
|
|
||||||
|
code_executor:
|
||||||
|
timeout: 5
|
||||||
|
sandbox: true
|
||||||
|
|
||||||
|
calculator:
|
||||||
|
precision: 10
|
||||||
|
|
||||||
|
# ── 记忆配置 ───────────────────────────────────────────────────
|
||||||
|
memory:
|
||||||
|
max_history: 20
|
||||||
|
enable_long_term: false
|
||||||
|
vector_db_url: ""
|
||||||
|
|
||||||
|
# ── 日志配置 ───────────────────────────────────────────────────
|
||||||
|
logging:
|
||||||
|
level: "DEBUG"
|
||||||
|
enable_file: true
|
||||||
|
log_dir: "./logs"
|
||||||
|
log_file: "agent.log"
|
||||||
|
|
||||||
|
# ── Agent 行为配置 ─────────────────────────────────────────────
|
||||||
|
agent:
|
||||||
|
max_chain_steps: 10
|
||||||
|
enable_multi_step: true
|
||||||
|
session_timeout: 3600
|
||||||
|
fallback_to_rules: true # API 调用失败时是否降级到规则引擎
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
"""
|
||||||
|
config/settings.py
|
||||||
|
配置加载与管理模块(新增 OpenAI 专用字段)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
_YAML_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_YAML_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# 配置数据类
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMConfig:
|
||||||
|
"""LLM 模型配置(含 OpenAI 专用字段)"""
|
||||||
|
provider: str = "openai"
|
||||||
|
model_name: str = "gpt-4o"
|
||||||
|
api_key: str = ""
|
||||||
|
api_base_url: str = ""
|
||||||
|
max_tokens: int = 4096
|
||||||
|
temperature: float = 0.7
|
||||||
|
timeout: int = 60
|
||||||
|
max_retries: int = 3
|
||||||
|
# OpenAI 专用
|
||||||
|
function_calling: bool = True
|
||||||
|
stream: bool = False
|
||||||
|
# Ollama / 本地模型
|
||||||
|
model_path: str = ""
|
||||||
|
ollama_host: str = "http://localhost:11434"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.api_key = os.getenv("LLM_API_KEY", self.api_key)
|
||||||
|
self.api_base_url = os.getenv("LLM_API_BASE_URL", self.api_base_url)
|
||||||
|
self.model_name = os.getenv("LLM_MODEL_NAME", self.model_name)
|
||||||
|
self.model_path = os.getenv("LLM_MODEL_PATH", self.model_path)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCPConfig:
|
||||||
|
server_name: str = "DemoMCPServer"
|
||||||
|
transport: str = "stdio"
|
||||||
|
host: str = "localhost"
|
||||||
|
port: int = 3000
|
||||||
|
enabled_tools: list[str] = field(default_factory=lambda: [
|
||||||
|
"calculator", "web_search", "file_reader", "code_executor"
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WebSearchToolConfig:
|
||||||
|
max_results: int = 5
|
||||||
|
timeout: int = 10
|
||||||
|
api_key: str = ""
|
||||||
|
engine: str = "mock"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.api_key = os.getenv("SEARCH_API_KEY", self.api_key)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FileReaderToolConfig:
|
||||||
|
allowed_root: str = "./workspace"
|
||||||
|
max_file_size_kb: int = 512
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CodeExecutorToolConfig:
|
||||||
|
timeout: int = 5
|
||||||
|
sandbox: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CalculatorToolConfig:
|
||||||
|
precision: int = 10
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolsConfig:
|
||||||
|
web_search: WebSearchToolConfig = field(default_factory=WebSearchToolConfig)
|
||||||
|
file_reader: FileReaderToolConfig = field(default_factory=FileReaderToolConfig)
|
||||||
|
code_executor: CodeExecutorToolConfig = field(default_factory=CodeExecutorToolConfig)
|
||||||
|
calculator: CalculatorToolConfig = field(default_factory=CalculatorToolConfig)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MemoryConfig:
|
||||||
|
max_history: int = 20
|
||||||
|
enable_long_term: bool = False
|
||||||
|
vector_db_url: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoggingConfig:
|
||||||
|
level: str = "DEBUG"
|
||||||
|
enable_file: bool = True
|
||||||
|
log_dir: str = "./logs"
|
||||||
|
log_file: str = "agent.log"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.level = os.getenv("LOG_LEVEL", self.level).upper()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentConfig:
|
||||||
|
max_chain_steps: int = 10
|
||||||
|
enable_multi_step: bool = True
|
||||||
|
session_timeout: int = 3600
|
||||||
|
fallback_to_rules: bool = True # API 失败时降级到规则引擎
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppConfig:
|
||||||
|
llm: LLMConfig = field(default_factory=LLMConfig)
|
||||||
|
mcp: MCPConfig = field(default_factory=MCPConfig)
|
||||||
|
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
||||||
|
memory: MemoryConfig = field(default_factory=MemoryConfig)
|
||||||
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
||||||
|
agent: AgentConfig = field(default_factory=AgentConfig)
|
||||||
|
|
||||||
|
def display(self) -> str:
|
||||||
|
lines = [
|
||||||
|
"─" * 52,
|
||||||
|
" 📋 当前配置",
|
||||||
|
"─" * 52,
|
||||||
|
f" [LLM] provider = {self.llm.provider}",
|
||||||
|
f" [LLM] model_name = {self.llm.model_name}",
|
||||||
|
f" [LLM] api_key = {'***' if self.llm.api_key else '(未设置)'}",
|
||||||
|
f" [LLM] api_base_url = {self.llm.api_base_url or '(默认)'}",
|
||||||
|
f" [LLM] temperature = {self.llm.temperature}",
|
||||||
|
f" [LLM] max_tokens = {self.llm.max_tokens}",
|
||||||
|
f" [LLM] function_calling = {self.llm.function_calling}",
|
||||||
|
f" [LLM] stream = {self.llm.stream}",
|
||||||
|
f" [LLM] max_retries = {self.llm.max_retries}",
|
||||||
|
f" [MCP] server_name = {self.mcp.server_name}",
|
||||||
|
f" [MCP] enabled_tools = {self.mcp.enabled_tools}",
|
||||||
|
f" [MEMORY] max_history = {self.memory.max_history}",
|
||||||
|
f" [AGENT] multi_step = {self.agent.enable_multi_step}",
|
||||||
|
f" [AGENT] fallback_rules = {self.agent.fallback_to_rules}",
|
||||||
|
f" [LOG] level = {self.logging.level}",
|
||||||
|
"─" * 52,
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# 配置加载器
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class ConfigLoader:
|
||||||
|
_CONFIG_SEARCH_PATHS = [
|
||||||
|
Path(os.getenv("AGENT_CONFIG_PATH", "./config.yaml")),
|
||||||
|
Path("config") / "config.yaml",
|
||||||
|
Path("config.yaml"),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls) -> AppConfig:
|
||||||
|
raw = cls._read_yaml()
|
||||||
|
return cls._parse(raw) if raw else AppConfig()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_yaml(cls) -> dict[str, Any] | None:
|
||||||
|
if not _YAML_AVAILABLE:
|
||||||
|
print("⚠️ PyYAML 未安装(pip install pyyaml),使用默认配置")
|
||||||
|
return None
|
||||||
|
for path in cls._CONFIG_SEARCH_PATHS:
|
||||||
|
if path and path.exists():
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
print(f"✅ 已加载配置文件: {path.resolve()}")
|
||||||
|
return data or {}
|
||||||
|
print("ℹ️ 未找到配置文件,使用默认配置")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, raw: dict[str, Any]) -> AppConfig:
|
||||||
|
return AppConfig(
|
||||||
|
llm=cls._parse_llm(raw.get("llm", {})),
|
||||||
|
mcp=cls._parse_mcp(raw.get("mcp", {})),
|
||||||
|
tools=cls._parse_tools(raw.get("tools", {})),
|
||||||
|
memory=cls._parse_memory(raw.get("memory", {})),
|
||||||
|
logging=cls._parse_logging(raw.get("logging", {})),
|
||||||
|
agent=cls._parse_agent(raw.get("agent", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_llm(d: dict) -> LLMConfig:
|
||||||
|
return LLMConfig(
|
||||||
|
provider=d.get("provider", "openai"),
|
||||||
|
model_name=d.get("model_name", "gpt-4o"),
|
||||||
|
api_key=d.get("api_key", ""),
|
||||||
|
api_base_url=d.get("api_base_url", ""),
|
||||||
|
max_tokens=int(d.get("max_tokens", 4096)),
|
||||||
|
temperature=float(d.get("temperature", 0.7)),
|
||||||
|
timeout=int(d.get("timeout", 60)),
|
||||||
|
max_retries=int(d.get("max_retries", 3)),
|
||||||
|
function_calling=bool(d.get("function_calling", True)),
|
||||||
|
stream=bool(d.get("stream", False)),
|
||||||
|
model_path=d.get("model_path", ""),
|
||||||
|
ollama_host=d.get("ollama_host", "http://localhost:11434"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_mcp(d: dict) -> MCPConfig:
|
||||||
|
return MCPConfig(
|
||||||
|
server_name=d.get("server_name", "DemoMCPServer"),
|
||||||
|
transport=d.get("transport", "stdio"),
|
||||||
|
host=d.get("host", "localhost"),
|
||||||
|
port=int(d.get("port", 3000)),
|
||||||
|
enabled_tools=d.get("enabled_tools", [
|
||||||
|
"calculator", "web_search", "file_reader", "code_executor"
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_tools(d: dict) -> ToolsConfig:
|
||||||
|
ws = d.get("web_search", {})
|
||||||
|
fr = d.get("file_reader", {})
|
||||||
|
ce = d.get("code_executor", {})
|
||||||
|
ca = d.get("calculator", {})
|
||||||
|
return ToolsConfig(
|
||||||
|
web_search=WebSearchToolConfig(
|
||||||
|
max_results=int(ws.get("max_results", 5)),
|
||||||
|
timeout=int(ws.get("timeout", 10)),
|
||||||
|
api_key=ws.get("api_key", ""),
|
||||||
|
engine=ws.get("engine", "mock"),
|
||||||
|
),
|
||||||
|
file_reader=FileReaderToolConfig(
|
||||||
|
allowed_root=fr.get("allowed_root", "./workspace"),
|
||||||
|
max_file_size_kb=int(fr.get("max_file_size_kb", 512)),
|
||||||
|
),
|
||||||
|
code_executor=CodeExecutorToolConfig(
|
||||||
|
timeout=int(ce.get("timeout", 5)),
|
||||||
|
sandbox=bool(ce.get("sandbox", True)),
|
||||||
|
),
|
||||||
|
calculator=CalculatorToolConfig(
|
||||||
|
precision=int(ca.get("precision", 10)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_memory(d: dict) -> MemoryConfig:
|
||||||
|
return MemoryConfig(
|
||||||
|
max_history=int(d.get("max_history", 20)),
|
||||||
|
enable_long_term=bool(d.get("enable_long_term", False)),
|
||||||
|
vector_db_url=d.get("vector_db_url", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_logging(d: dict) -> LoggingConfig:
|
||||||
|
return LoggingConfig(
|
||||||
|
level=d.get("level", "DEBUG"),
|
||||||
|
enable_file=bool(d.get("enable_file", True)),
|
||||||
|
log_dir=d.get("log_dir", "./logs"),
|
||||||
|
log_file=d.get("log_file", "agent.log"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_agent(d: dict) -> AgentConfig:
|
||||||
|
return AgentConfig(
|
||||||
|
max_chain_steps=int(d.get("max_chain_steps", 10)),
|
||||||
|
enable_multi_step=bool(d.get("enable_multi_step", True)),
|
||||||
|
session_timeout=int(d.get("session_timeout", 3600)),
|
||||||
|
fallback_to_rules=bool(d.get("fallback_to_rules", True)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例
|
||||||
|
settings: AppConfig = ConfigLoader.load()
|
||||||
|
|
@ -1,36 +1,30 @@
|
||||||
"""LLM 引擎:意图理解 & 工具决策"""
|
|
||||||
"""
|
"""
|
||||||
llm/llm_engine.py
|
llm/llm_engine.py
|
||||||
LLM 引擎:负责意图理解、工具选择决策、最终回复生成
|
LLM 引擎:通过 Provider 接口调用真实 OpenAI API
|
||||||
生产环境可替换 _call_llm_api() 为真实 API 调用(OpenAI / Anthropic 等)
|
支持 Function Calling 多步骤规划 + 工具结果整合回复
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from mcp.mcp_protocol import MCPRequest, MCPMethod, ToolSchema
|
from config.settings import LLMConfig, settings
|
||||||
|
from llm.provider_factory import create_provider
|
||||||
|
from llm.providers.base_provider import BaseProvider
|
||||||
|
from mcp.mcp_protocol import ChainPlan, MCPMethod, MCPRequest, ToolSchema, ToolStep
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
from openai import OpenAI
|
|
||||||
import os
|
|
||||||
|
|
||||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
|
||||||
|
|
||||||
# ── 工具调用决策结果 ───────────────────────────────────────────
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ToolDecision:
|
class ToolDecision:
|
||||||
"""LLM 决策是否调用工具及调用参数"""
|
|
||||||
need_tool: bool
|
need_tool: bool
|
||||||
tool_name: str = ""
|
tool_name: str = ""
|
||||||
arguments: dict = None
|
arguments: dict = None
|
||||||
reasoning: str = "" # 推理过程说明
|
reasoning: str = ""
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.arguments = self.arguments or {}
|
self.arguments = self.arguments or {}
|
||||||
|
|
||||||
def to_mcp_request(self) -> MCPRequest | None:
|
def to_mcp_request(self) -> MCPRequest | None:
|
||||||
"""将工具决策转换为 MCP 请求"""
|
|
||||||
if not self.need_tool:
|
if not self.need_tool:
|
||||||
return None
|
return None
|
||||||
return MCPRequest(
|
return MCPRequest(
|
||||||
|
|
@ -39,246 +33,347 @@ class ToolDecision:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MonicaClient:
|
|
||||||
BASE_URL = "https://openapi.monica.im/v1"
|
|
||||||
|
|
||||||
def __init__(self, api_key):
|
|
||||||
self.client = OpenAI(base_url=self.BASE_URL,
|
|
||||||
api_key=api_key)
|
|
||||||
self.logger = get_logger("Monica")
|
|
||||||
|
|
||||||
def create(self, model_name: str, tool_schemas, user_input: str, agent_prompt: str = "") -> ToolDecision:
|
|
||||||
tools = [{
|
|
||||||
"name": s.name,
|
|
||||||
"description": s.description,
|
|
||||||
"parameters": s.parameters} for s in tool_schemas]
|
|
||||||
messages = []
|
|
||||||
if agent_prompt:
|
|
||||||
messages.append({
|
|
||||||
"role": "system",
|
|
||||||
"content": agent_prompt,
|
|
||||||
})
|
|
||||||
messages.append({
|
|
||||||
"role": "user",
|
|
||||||
"content": [{
|
|
||||||
"type": "text",
|
|
||||||
"text": user_input
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
completion = self.client.chat.completions.create(
|
|
||||||
model=model_name,
|
|
||||||
functions=tools,
|
|
||||||
messages=messages
|
|
||||||
)
|
|
||||||
self.logger.info(completion.choices[0].message.content)
|
|
||||||
response = json.loads(completion.choices[0].message.content)
|
|
||||||
return ToolDecision(need_tool=response['need_tool'],
|
|
||||||
tool_name=response['tool_name'],
|
|
||||||
arguments=response['arguments'],
|
|
||||||
reasoning=response['reasoning'])
|
|
||||||
|
|
||||||
def chat(self, model_name: str, user_input: str, context: str = '') -> str:
|
|
||||||
message = f"""##用户输入\n{user_input}\n\n"""\
|
|
||||||
f"""##历史消息\n{context}\n\n"""
|
|
||||||
|
|
||||||
messages = [{
|
|
||||||
"role": "user",
|
|
||||||
"content": [{
|
|
||||||
"type": "text",
|
|
||||||
"text": message
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
completion = self.client.chat.completions.create(
|
|
||||||
model=model_name,
|
|
||||||
messages=messages
|
|
||||||
)
|
|
||||||
self.logger.info(completion.choices[0].message.content)
|
|
||||||
return completion.choices[0].message.content
|
|
||||||
|
|
||||||
|
|
||||||
# ── LLM 引擎 ──────────────────────────────────────────────────
|
|
||||||
class LLMEngine:
|
class LLMEngine:
|
||||||
"""
|
"""
|
||||||
LLM 推理引擎(ReAct 模式)
|
LLM 推理引擎(Provider 模式)
|
||||||
|
|
||||||
执行流程:
|
核心流程:
|
||||||
1. 接收用户输入 + 工具列表
|
1. plan_tool_chain()
|
||||||
2. 分析意图,决策是否调用工具(think)
|
构造 OpenAI 格式消息 + tools
|
||||||
3. 若需要工具,生成 MCPRequest(act)
|
→ Provider.plan_with_tools()
|
||||||
4. 接收工具结果,生成最终回复(observe)
|
→ 解析 tool_calls → ChainPlan
|
||||||
|
|
||||||
生产环境替换:
|
2. generate_chain_reply()
|
||||||
将 _call_llm_api() 替换为真实 LLM API 调用即可,
|
构造含工具结果的完整消息历史
|
||||||
其余流程控制逻辑保持不变。
|
→ Provider.generate_reply()
|
||||||
|
→ 最终自然语言回复
|
||||||
|
|
||||||
|
降级策略:
|
||||||
|
API 调用失败 且 fallback_to_rules=true
|
||||||
|
→ 自动切换到规则引擎(保证系统可用性)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
API_KEY = "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR"
|
# 规则引擎关键词(降级时使用)
|
||||||
|
_MULTI_STEP_KEYWORDS = [
|
||||||
|
"然后", "接着", "再", "并且", "同时", "之后",
|
||||||
|
"先.*再", "首先.*然后", "搜索.*计算", "读取.*执行",
|
||||||
|
"多个", "分别", "依次",
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, model_name: str = "claude-sonnet-4-6"):
|
def __init__(self, cfg: LLMConfig | None = None):
|
||||||
self.model_name = model_name
|
self.cfg = cfg or settings.llm
|
||||||
self.logger = get_logger("LLM")
|
self.logger = get_logger("LLM")
|
||||||
self.logger.info(f"🧠 LLM 引擎初始化,模型: {model_name}")
|
self.provider: BaseProvider = create_provider(self.cfg)
|
||||||
self.client = MonicaClient(api_key=self.API_KEY)
|
self._log_init()
|
||||||
|
|
||||||
# ── 核心推理流程 ────────────────────────────────────────────
|
def _log_init(self) -> None:
|
||||||
|
self.logger.info("🧠 LLM 引擎初始化完成")
|
||||||
|
self.logger.info(f" provider = {self.cfg.provider}")
|
||||||
|
self.logger.info(f" model_name = {self.cfg.model_name}")
|
||||||
|
self.logger.info(f" function_calling = {self.cfg.function_calling}")
|
||||||
|
self.logger.info(f" temperature = {self.cfg.temperature}")
|
||||||
|
self.logger.info(f" fallback_rules = {settings.agent.fallback_to_rules}")
|
||||||
|
|
||||||
def think_and_decide(
|
def reconfigure(self, cfg: LLMConfig) -> None:
|
||||||
self,
|
"""热更新配置并重建 Provider"""
|
||||||
user_input: str,
|
self.cfg = cfg
|
||||||
tool_schemas: list[ToolSchema],
|
self.provider = create_provider(cfg)
|
||||||
context: str = "",
|
self.logger.info(f"🔄 LLM 配置已更新: model={cfg.model_name}")
|
||||||
agent_prompt: str = ""
|
|
||||||
) -> ToolDecision:
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 核心接口
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def plan_tool_chain(
|
||||||
|
self,
|
||||||
|
user_input: str,
|
||||||
|
tool_schemas: list[ToolSchema],
|
||||||
|
context: str = "",
|
||||||
|
history: list[dict] | None = None,
|
||||||
|
) -> ChainPlan:
|
||||||
"""
|
"""
|
||||||
Step 1 & 2: 理解意图,决策工具调用(Think 阶段)
|
使用 OpenAI Function Calling 规划工具调用链
|
||||||
|
|
||||||
|
消息构造策略:
|
||||||
|
system → 规划器系统提示
|
||||||
|
history → 历史对话(可选)
|
||||||
|
user → 当前用户输入
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_input: 用户输入文本
|
user_input: 用户输入文本
|
||||||
tool_schemas: 可用工具的 Schema 列表
|
tool_schemas: 可用工具列表
|
||||||
context: 对话历史上下文摘要
|
context: 对话历史摘要(文本格式,用于无 history 时)
|
||||||
agent_prompt: 智能体提示词
|
history: 结构化对话历史(OpenAI 消息格式,优先使用)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ToolDecision 实例
|
ChainPlan 实例
|
||||||
"""
|
"""
|
||||||
self.logger.info(f"💭 分析意图: {user_input[:50]}...")
|
self.logger.info(f"🗺 规划工具调用链: {user_input[:60]}...")
|
||||||
|
|
||||||
# 构造 Prompt(生产环境发送给真实 LLM)
|
# 构造消息列表
|
||||||
prompt = self._build_decision_prompt(user_input, tool_schemas, context)
|
messages = self._build_plan_messages(user_input, context, history)
|
||||||
self.logger.debug(f"📝 Prompt 已构造 ({len(prompt)} chars)")
|
|
||||||
|
|
||||||
# 调用 LLM(Demo 中使用规则模拟)
|
if self.cfg.function_calling:
|
||||||
# decision = self._call_llm_api(user_input, tool_schemas)
|
# ── 真实 OpenAI Function Calling ──────────────────
|
||||||
decision = self._call_llm_api(prompt, tool_schemas, agent_prompt=agent_prompt)
|
result = self.provider.plan_with_tools(messages, tool_schemas)
|
||||||
|
|
||||||
self.logger.info(
|
if result.success and result.plan is not None:
|
||||||
f"🎯 决策结果: {'调用工具 [' + decision.tool_name + ']' if decision.need_tool else '直接回复'}"
|
plan = result.plan
|
||||||
|
# 补充 goal 字段
|
||||||
|
if not plan.goal:
|
||||||
|
plan.goal = user_input
|
||||||
|
self.logger.info(f"📋 OpenAI 规划完成: {plan.step_count} 步")
|
||||||
|
for step in plan.steps:
|
||||||
|
self.logger.info(
|
||||||
|
f" Step {step.step_id}: [{step.tool_name}] "
|
||||||
|
f"args={step.arguments}"
|
||||||
|
)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
# API 失败处理
|
||||||
|
self.logger.warning(f"⚠️ OpenAI 规划失败: {result.error}")
|
||||||
|
if settings.agent.fallback_to_rules:
|
||||||
|
self.logger.info("🔄 降级到规则引擎...")
|
||||||
|
return self._rule_based_plan(user_input)
|
||||||
|
return ChainPlan(goal=user_input, steps=[])
|
||||||
|
|
||||||
|
else:
|
||||||
|
# function_calling=false 时直接使用规则引擎
|
||||||
|
self.logger.info("⚙️ function_calling=false,使用规则引擎")
|
||||||
|
return self._rule_based_plan(user_input)
|
||||||
|
|
||||||
|
def think_and_decide(
|
||||||
|
self,
|
||||||
|
user_input: str,
|
||||||
|
tool_schemas: list[ToolSchema],
|
||||||
|
context: str = "",
|
||||||
|
) -> ToolDecision:
|
||||||
|
"""单步工具决策(代理到 plan_tool_chain)"""
|
||||||
|
plan = self.plan_tool_chain(user_input, tool_schemas, context)
|
||||||
|
if not plan.steps:
|
||||||
|
return ToolDecision(need_tool=False, reasoning="无需工具,直接回复")
|
||||||
|
first = plan.steps[0]
|
||||||
|
return ToolDecision(
|
||||||
|
need_tool=True,
|
||||||
|
tool_name=first.tool_name,
|
||||||
|
arguments=first.arguments,
|
||||||
|
reasoning=first.description,
|
||||||
)
|
)
|
||||||
self.logger.debug(f"💡 推理: {decision.reasoning}")
|
|
||||||
return decision
|
|
||||||
|
|
||||||
def generate_final_reply(
|
def generate_chain_reply(
|
||||||
self,
|
self,
|
||||||
user_input: str,
|
user_input: str,
|
||||||
tool_name: str,
|
chain_summary: str,
|
||||||
tool_output: str,
|
context: str = "",
|
||||||
context: str = "",
|
tool_messages: list[dict] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Step 5: 整合工具结果,生成最终自然语言回复(Observe 阶段)
|
整合多步骤执行结果,调用 OpenAI 生成最终自然语言回复
|
||||||
|
|
||||||
|
消息构造(含工具执行结果):
|
||||||
|
system → 回复生成系统提示
|
||||||
|
user → 原始用户输入
|
||||||
|
assistant → 工具调用决策(tool_calls)
|
||||||
|
tool → 工具执行结果
|
||||||
|
...(多轮工具调用)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_input: 原始用户输入
|
user_input: 原始用户输入
|
||||||
tool_name: 被调用的工具名称
|
chain_summary: 步骤摘要(API 失败时的降级内容)
|
||||||
tool_output: 工具返回的原始输出
|
context: 对话历史
|
||||||
context: 对话历史上下文
|
tool_messages: 完整的工具调用消息序列(OpenAI 格式)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
最终回复字符串
|
最终回复字符串
|
||||||
"""
|
"""
|
||||||
self.logger.info("✍️ 整合工具结果,生成最终回复...")
|
self.logger.info("✍️ 生成最终回复...")
|
||||||
|
|
||||||
# 生产环境:将 tool_output 注入 Prompt,调用 LLM 生成回复
|
if tool_messages:
|
||||||
reply = self._synthesize_reply(user_input, tool_name, tool_output)
|
# 构造含工具结果的完整消息历史
|
||||||
self.logger.info(f"💬 回复已生成 ({len(reply)} chars)")
|
messages = self._build_reply_messages(user_input, tool_messages)
|
||||||
return reply
|
result = self.provider.generate_reply(messages)
|
||||||
|
|
||||||
|
if result.success and result.content:
|
||||||
|
self.logger.info(
|
||||||
|
f"✅ OpenAI 回复生成成功 ({len(result.content)} chars)"
|
||||||
|
)
|
||||||
|
return result.content
|
||||||
|
|
||||||
|
self.logger.warning(f"⚠️ OpenAI 回复生成失败: {result.error}")
|
||||||
|
|
||||||
|
# 降级:使用模板回复
|
||||||
|
return self._fallback_chain_reply(user_input, chain_summary)
|
||||||
|
|
||||||
|
def generate_final_reply(
|
||||||
|
self,
|
||||||
|
user_input: str,
|
||||||
|
tool_name: str,
|
||||||
|
tool_output: str,
|
||||||
|
context: str = "",
|
||||||
|
tool_call_id: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""单步工具结果整合(调用 OpenAI 生成自然语言回复)"""
|
||||||
|
self.logger.info(f"✍️ 整合单步工具结果 [{tool_name}]...")
|
||||||
|
|
||||||
|
# 构造单步工具消息
|
||||||
|
tool_messages = []
|
||||||
|
if tool_call_id:
|
||||||
|
tool_messages = [
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": tool_output,
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.generate_chain_reply(
|
||||||
|
user_input=user_input,
|
||||||
|
chain_summary=tool_output,
|
||||||
|
context=context,
|
||||||
|
tool_messages=tool_messages,
|
||||||
|
)
|
||||||
|
|
||||||
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
|
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
|
||||||
"""无需工具时直接生成回复"""
|
"""无需工具时直接调用 OpenAI 生成回复"""
|
||||||
self.logger.info("💬 直接生成回复(无需工具)")
|
self.logger.info("💬 直接生成回复(无需工具)...")
|
||||||
return self.client.chat(self.model_name, user_input, context=context)
|
messages = [
|
||||||
|
{"role": "system", "content": "你是一个友好、专业的 AI 助手,请简洁准确地回答用户问题。"},
|
||||||
# ── Prompt 构造 ─────────────────────────────────────────────
|
{"role": "user", "content": user_input},
|
||||||
|
]
|
||||||
def _build_decision_prompt(
|
result = self.provider.generate_reply(messages)
|
||||||
self,
|
if result.success and result.content:
|
||||||
user_input: str,
|
return result.content
|
||||||
tool_schemas: list[ToolSchema],
|
# 降级
|
||||||
context: str,
|
|
||||||
) -> str:
|
|
||||||
"""构造工具决策 Prompt(ReAct 格式)"""
|
|
||||||
tools_desc = "\n".join(
|
|
||||||
f"- {s.name}: {s.description}" for s in tool_schemas
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
f"你是一个智能助手,请分析用户输入并决定是否需要调用工具。\n\n"
|
f"[{self.cfg.model_name}] 您好!\n"
|
||||||
f"## 可用工具\n{tools_desc}\n\n"
|
f"关于「{user_input}」,我已收到您的问题。\n"
|
||||||
f"## 对话历史\n{context or '(无)'}\n\n"
|
f"(API 暂时不可用,请检查 API Key 配置)"
|
||||||
f"## 用户输入\n{user_input}\n\n"
|
|
||||||
f"## 指令\n"
|
|
||||||
f"以纯 JSON 格式回复,不要嵌入到其他对象中,如下:\n"
|
|
||||||
f'{{"need_tool": true/false, "tool_name": "...", "arguments": {{...}}, "reasoning": "..."}}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 模拟 LLM API(Demo 用规则引擎替代)────────────────────
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 消息构造
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _call_llm_api(self, user_input: str, tool_schemas: list[ToolSchema], agent_prompt: str = "") -> ToolDecision:
|
@staticmethod
|
||||||
"""
|
def _build_plan_messages(
|
||||||
模拟 LLM API 调用(Demo 版本使用关键词规则)
|
user_input: str,
|
||||||
|
context: str,
|
||||||
|
history: list[dict] | None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""构造规划阶段的消息列表"""
|
||||||
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
messages: list[dict] = [
|
||||||
|
{"role": "system", "content": OpenAIProvider._PLANNER_SYSTEM_PROMPT},
|
||||||
|
]
|
||||||
|
# 注入结构化历史(优先)或文本摘要
|
||||||
|
if history:
|
||||||
|
messages.extend(history[-6:]) # 最近 3 轮
|
||||||
|
elif context and context != "(暂无对话历史)":
|
||||||
|
messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": f"## 对话历史\n{context}",
|
||||||
|
})
|
||||||
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
return messages
|
||||||
|
|
||||||
生产环境替换示例:
|
@staticmethod
|
||||||
import anthropic
|
def _build_reply_messages(
|
||||||
client = anthropic.Anthropic()
|
user_input: str,
|
||||||
response = client.messages.create(
|
tool_messages: list[dict],
|
||||||
model=self.model_name,
|
) -> list[dict]:
|
||||||
tools=[s.to_dict() for s in tool_schemas],
|
"""构造回复生成阶段的消息列表(含工具执行结果)"""
|
||||||
messages=[{"role": "user", "content": user_input}]
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
messages: list[dict] = [
|
||||||
|
{"role": "system", "content": OpenAIProvider._REPLY_SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_input},
|
||||||
|
]
|
||||||
|
messages.extend(tool_messages)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 降级规则引擎
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _rule_based_plan(self, user_input: str) -> ChainPlan:
|
||||||
|
"""规则引擎(API 不可用时的降级方案)"""
|
||||||
|
self.logger.info("⚙️ 使用规则引擎规划...")
|
||||||
|
text = user_input.lower()
|
||||||
|
|
||||||
|
# 搜索 + 计算
|
||||||
|
if (any(k in text for k in ["搜索", "查询", "查一下"]) and
|
||||||
|
any(k in text for k in ["计算", "算", "等于", "结果"])):
|
||||||
|
return ChainPlan(
|
||||||
|
goal=user_input,
|
||||||
|
steps=[
|
||||||
|
ToolStep(1, "web_search",
|
||||||
|
{"query": user_input,
|
||||||
|
"max_results": settings.tools.web_search.max_results},
|
||||||
|
"搜索相关信息", []),
|
||||||
|
ToolStep(2, "calculator",
|
||||||
|
{"expression": self._extract_expression(user_input)},
|
||||||
|
"进行计算", [1]),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
# 解析 response.content 中的 tool_use block
|
# 读取文件 + 执行代码
|
||||||
"""
|
if (any(k in text for k in ["读取", "文件", "file"]) and
|
||||||
if self.client:
|
any(k in text for k in ["执行", "运行", "run"])):
|
||||||
return self.client.create(self.model_name,
|
fname = re.search(r"[\w\-\.]+\.\w+", user_input)
|
||||||
user_input=user_input,
|
return ChainPlan(
|
||||||
tool_schemas=tool_schemas,
|
goal=user_input,
|
||||||
agent_prompt=agent_prompt)
|
steps=[
|
||||||
else:
|
ToolStep(1, "file_reader",
|
||||||
text = user_input.lower()
|
{"path": fname.group() if fname else "script.py"},
|
||||||
# 规则匹配:计算器
|
"读取文件", []),
|
||||||
calc_pattern = re.search(r"[\d\s\+\-\*\/\(\)\^]+[=??]?", user_input)
|
ToolStep(2, "code_executor",
|
||||||
if any(kw in text for kw in ["计算", "等于", "多少", "×", "÷"]) and calc_pattern:
|
{"code": "{{STEP_1_OUTPUT}}",
|
||||||
expr = re.sub(r"[^0-9+\-*/().**]", "", user_input.replace("×", "*").replace("÷", "/"))
|
"timeout": settings.tools.code_executor.timeout},
|
||||||
return ToolDecision(
|
"执行代码", [1]),
|
||||||
need_tool=True, tool_name="calculator",
|
],
|
||||||
arguments={"expression": expr or "1+1"},
|
|
||||||
reasoning="用户请求数学计算,调用 calculator 工具",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 规则匹配:搜索
|
|
||||||
if any(kw in text for kw in ["搜索", "查询", "天气", "新闻", "查一下", "search"]):
|
|
||||||
return ToolDecision(
|
|
||||||
need_tool=True, tool_name="web_search",
|
|
||||||
arguments={"query": user_input, "max_results": 3},
|
|
||||||
reasoning="用户需要实时信息,调用 web_search 工具",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 规则匹配:文件读取
|
|
||||||
if any(kw in text for kw in ["文件", "读取", "file", "config", "json", "txt"]):
|
|
||||||
filename = re.search(r"[\w\-\.]+\.\w+", user_input)
|
|
||||||
return ToolDecision(
|
|
||||||
need_tool=True, tool_name="file_reader",
|
|
||||||
arguments={"path": filename.group() if filename else "config.json"},
|
|
||||||
reasoning="用户请求读取文件,调用 file_reader 工具",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 规则匹配:代码执行
|
|
||||||
if any(kw in text for kw in ["执行", "运行", "代码", "python", "print", "code"]):
|
|
||||||
code_match = re.search(r'[`\'"](.+?)[`\'"]', user_input)
|
|
||||||
code = code_match.group(1) if code_match else 'print("Hello, Agent!")'
|
|
||||||
return ToolDecision(
|
|
||||||
need_tool=True, tool_name="code_executor",
|
|
||||||
arguments={"code": code, "timeout": 5},
|
|
||||||
reasoning="用户请求执行代码,调用 code_executor 工具",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 默认:直接回复
|
|
||||||
return ToolDecision(
|
|
||||||
need_tool=False,
|
|
||||||
reasoning="问题可直接回答,无需工具",
|
|
||||||
)
|
)
|
||||||
|
return self._rule_single_step(user_input)
|
||||||
|
|
||||||
def _synthesize_reply(self, user_input: str, tool_name: str, tool_output: str) -> str:
|
def _rule_single_step(self, user_input: str) -> ChainPlan:
|
||||||
"""基于工具输出合成最终回复(Demo 版本)"""
|
"""单步规则匹配"""
|
||||||
|
text = user_input.lower()
|
||||||
|
if any(k in text for k in ["计算", "等于", "×", "÷", "+", "-", "*", "/"]):
|
||||||
|
expr = self._extract_expression(user_input)
|
||||||
|
return ChainPlan(goal=user_input, is_single=True,
|
||||||
|
steps=[ToolStep(1, "calculator",
|
||||||
|
{"expression": expr}, "数学计算")])
|
||||||
|
if any(k in text for k in ["搜索", "查询", "天气", "新闻"]):
|
||||||
|
return ChainPlan(goal=user_input, is_single=True,
|
||||||
|
steps=[ToolStep(1, "web_search",
|
||||||
|
{"query": user_input,
|
||||||
|
"max_results": settings.tools.web_search.max_results},
|
||||||
|
"网络搜索")])
|
||||||
|
if any(k in text for k in ["文件", "读取", "file"]):
|
||||||
|
fname = re.search(r"[\w\-\.]+\.\w+", user_input)
|
||||||
|
return ChainPlan(goal=user_input, is_single=True,
|
||||||
|
steps=[ToolStep(1, "file_reader",
|
||||||
|
{"path": fname.group() if fname else "config.json"},
|
||||||
|
"读取文件")])
|
||||||
|
if any(k in text for k in ["执行", "运行", "代码", "python"]):
|
||||||
|
code_m = re.search(r"[`'\"](.+?)[`'\"]", user_input)
|
||||||
|
code = code_m.group(1) if code_m else 'print("Hello, Agent!")'
|
||||||
|
return ChainPlan(goal=user_input, is_single=True,
|
||||||
|
steps=[ToolStep(1, "code_executor",
|
||||||
|
{"code": code,
|
||||||
|
"timeout": settings.tools.code_executor.timeout},
|
||||||
|
"执行代码")])
|
||||||
|
return ChainPlan(goal=user_input, is_single=True, steps=[])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fallback_chain_reply(user_input: str, chain_summary: str) -> str:
|
||||||
|
"""API 不可用时的模板回复"""
|
||||||
return (
|
return (
|
||||||
f"✅ 已通过 [{tool_name}] 工具处理您的请求。\n\n"
|
f"✅ **任务已完成**\n\n"
|
||||||
f"**执行结果:**\n{tool_output}\n\n"
|
f"针对您的需求「{user_input}」,执行结果如下:\n\n"
|
||||||
f"---\n*由 {self.model_name} 生成 · 工具: {tool_name}*"
|
f"{chain_summary}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_expression(text: str) -> str:
|
||||||
|
cleaned = text.replace("×", "*").replace("÷", "/").replace(",", "")
|
||||||
|
match = re.search(r"[\d\s\+\-\*\/\(\)\.]+", cleaned)
|
||||||
|
expr = match.group().strip() if match else "1+1"
|
||||||
|
return expr if len(expr) > 1 else "1+1"
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
llm/provider_factory.py
|
||||||
|
Provider 工厂:根据 settings.llm.provider 自动实例化对应 Provider
|
||||||
|
"""
|
||||||
|
|
||||||
|
from config.settings import LLMConfig, settings
|
||||||
|
from llm.providers.base_provider import BaseProvider
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
_logger = get_logger("LLM")
|
||||||
|
|
||||||
|
|
||||||
|
def create_provider(cfg: LLMConfig | None = None) -> BaseProvider:
|
||||||
|
"""
|
||||||
|
工厂函数:根据配置创建对应的 LLM Provider
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cfg: LLMConfig 实例,None 时从全局 settings 读取
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseProvider 子类实例
|
||||||
|
|
||||||
|
支持的 provider:
|
||||||
|
- "openai" → OpenAIProvider(含兼容 OpenAI 协议的代理)
|
||||||
|
- "anthropic" → AnthropicProvider(预留)
|
||||||
|
- "ollama" → OllamaProvider(预留)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: provider 名称不支持时
|
||||||
|
"""
|
||||||
|
cfg = cfg or settings.llm
|
||||||
|
_logger.info(f"🏭 Provider 工厂: 创建 [{cfg.provider}] Provider")
|
||||||
|
|
||||||
|
match cfg.provider.lower():
|
||||||
|
|
||||||
|
case "openai":
|
||||||
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
return OpenAIProvider(cfg)
|
||||||
|
|
||||||
|
case "anthropic":
|
||||||
|
# 预留:Anthropic Provider
|
||||||
|
# from llm.providers.anthropic_provider import AnthropicProvider
|
||||||
|
# return AnthropicProvider(cfg)
|
||||||
|
_logger.warning("⚠️ Anthropic Provider 尚未实现,回退到 OpenAI")
|
||||||
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
return OpenAIProvider(cfg)
|
||||||
|
|
||||||
|
case "ollama":
|
||||||
|
# 预留:Ollama 本地模型 Provider
|
||||||
|
# from llm.providers.ollama_provider import OllamaProvider
|
||||||
|
# return OllamaProvider(cfg)
|
||||||
|
_logger.warning("⚠️ Ollama Provider 尚未实现,回退到 OpenAI")
|
||||||
|
from llm.providers.openai_provider import OpenAIProvider
|
||||||
|
return OpenAIProvider(cfg)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
f"不支持的 provider: '{cfg.provider}',"
|
||||||
|
f"可选值: openai / anthropic / ollama"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""
|
||||||
|
llm/providers/base_provider.py
|
||||||
|
LLM Provider 抽象基类:定义所有 Provider 必须实现的统一接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mcp.mcp_protocol import ChainPlan, ToolSchema
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# Provider 返回数据结构
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanResult:
|
||||||
|
"""
|
||||||
|
工具调用链规划结果
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
plan: 解析出的 ChainPlan(成功时)
|
||||||
|
raw_response: 原始 API 响应(用于调试)
|
||||||
|
usage: Token 用量统计
|
||||||
|
success: 是否成功
|
||||||
|
error: 失败原因
|
||||||
|
"""
|
||||||
|
plan: ChainPlan | None
|
||||||
|
raw_response: Any = None
|
||||||
|
usage: dict[str, int] = field(default_factory=dict)
|
||||||
|
success: bool = True
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt_tokens(self) -> int:
|
||||||
|
return self.usage.get("prompt_tokens", 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completion_tokens(self) -> int:
|
||||||
|
return self.usage.get("completion_tokens", 0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReplyResult:
|
||||||
|
"""
|
||||||
|
最终回复生成结果
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
content: 生成的自然语言回复
|
||||||
|
usage: Token 用量统计
|
||||||
|
success: 是否成功
|
||||||
|
error: 失败原因
|
||||||
|
"""
|
||||||
|
content: str
|
||||||
|
usage: dict[str, int] = field(default_factory=dict)
|
||||||
|
success: bool = True
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# 抽象基类
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class BaseProvider(ABC):
|
||||||
|
"""
|
||||||
|
LLM Provider 抽象基类
|
||||||
|
|
||||||
|
所有具体 Provider(OpenAI / Anthropic / Ollama)必须继承此类
|
||||||
|
并实现以下两个核心方法:
|
||||||
|
- plan_with_tools() 工具调用链规划(Function Calling)
|
||||||
|
- generate_reply() 最终回复生成
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def provider_name(self) -> str:
|
||||||
|
"""Provider 名称标识,如 'openai' / 'anthropic'"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def plan_with_tools(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
tool_schemas: list[ToolSchema],
|
||||||
|
) -> PlanResult:
|
||||||
|
"""
|
||||||
|
使用 Function Calling 规划工具调用链
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: 对话历史消息列表(OpenAI 格式)
|
||||||
|
tool_schemas: 可用工具的 Schema 列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlanResult 实例
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_reply(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
) -> ReplyResult:
|
||||||
|
"""
|
||||||
|
基于完整对话历史(含工具执行结果)生成最终回复
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: 包含 tool 角色消息的完整对话历史
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReplyResult 实例
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def health_check(self) -> bool:
|
||||||
|
"""
|
||||||
|
连通性检测(可选实现)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示 API 可用
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}(provider={self.provider_name})"
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
"""
|
||||||
|
llm/providers/openai_provider.py
|
||||||
|
OpenAI Provider:使用 Function Calling 实现工具链规划与回复生成
|
||||||
|
|
||||||
|
核心流程:
|
||||||
|
1. plan_with_tools()
|
||||||
|
messages + tools → OpenAI API
|
||||||
|
→ 解析 tool_calls → ChainPlan
|
||||||
|
|
||||||
|
2. generate_reply()
|
||||||
|
messages(含 tool 结果)→ OpenAI API
|
||||||
|
→ 最终自然语言回复
|
||||||
|
|
||||||
|
依赖:
|
||||||
|
pip install openai>=1.0.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from config.settings import LLMConfig
|
||||||
|
from llm.providers.base_provider import BaseProvider, PlanResult, ReplyResult
|
||||||
|
from mcp.mcp_protocol import ChainPlan, ToolSchema, ToolStep
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
# OpenAI SDK(运行时导入,避免未安装时整体崩溃)
|
||||||
|
try:
|
||||||
|
from openai import (
|
||||||
|
APIConnectionError,
|
||||||
|
APIStatusError,
|
||||||
|
APITimeoutError,
|
||||||
|
AuthenticationError,
|
||||||
|
OpenAI,
|
||||||
|
RateLimitError,
|
||||||
|
)
|
||||||
|
_OPENAI_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_OPENAI_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIProvider(BaseProvider):
|
||||||
|
"""
|
||||||
|
OpenAI Provider 实现
|
||||||
|
|
||||||
|
支持:
|
||||||
|
- 标准 OpenAI API(gpt-4o / gpt-4-turbo / gpt-3.5-turbo 等)
|
||||||
|
- 兼容 OpenAI 协议的第三方代理(通过 api_base_url 配置)
|
||||||
|
- Function Calling 多工具并行/串行规划
|
||||||
|
- 自动重试(RateLimit / 网络超时)
|
||||||
|
- API 不可用时降级到规则引擎
|
||||||
|
|
||||||
|
配置示例(config.yaml):
|
||||||
|
llm:
|
||||||
|
provider: "openai"
|
||||||
|
model_name: "gpt-4o"
|
||||||
|
api_key: "sk-..."
|
||||||
|
api_base_url: "" # 留空使用官方地址
|
||||||
|
temperature: 0.2 # 规划任务建议低温度
|
||||||
|
function_calling: true
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 系统 Prompt:指导 LLM 进行多步骤工具规划
|
||||||
|
_PLANNER_SYSTEM_PROMPT = """\
|
||||||
|
你是一个智能任务规划助手,擅长将用户需求分解为多个工具调用步骤。
|
||||||
|
|
||||||
|
## 工作原则
|
||||||
|
1. 仔细分析用户需求,判断是否需要调用工具
|
||||||
|
2. 如需多个工具,按逻辑顺序依次调用
|
||||||
|
3. 当后续步骤依赖前步结果时,先完成前步再继续
|
||||||
|
4. 每次只规划并调用当前最合适的工具
|
||||||
|
5. 所有工具执行完毕后,整合结果给出最终回复
|
||||||
|
|
||||||
|
## 重要规则
|
||||||
|
- 数学计算必须使用 calculator 工具,不要自行计算
|
||||||
|
- 需要实时信息时使用 web_search 工具
|
||||||
|
- 文件操作使用 file_reader 工具
|
||||||
|
- 代码执行使用 code_executor 工具
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 回复生成系统 Prompt
|
||||||
|
_REPLY_SYSTEM_PROMPT = """\
|
||||||
|
你是一个友好、专业的 AI 助手。
|
||||||
|
请基于已执行的工具调用结果,用清晰、自然的语言回答用户的问题。
|
||||||
|
回复要简洁明了,重点突出工具执行的关键结果。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cfg: LLMConfig):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.logger = get_logger("LLM")
|
||||||
|
self._client: "OpenAI | None" = None
|
||||||
|
|
||||||
|
if not _OPENAI_AVAILABLE:
|
||||||
|
self.logger.warning("⚠️ openai 包未安装,请执行: pip install openai>=1.0.0")
|
||||||
|
else:
|
||||||
|
self._init_client()
|
||||||
|
|
||||||
|
# ── Provider 标识 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider_name(self) -> str:
|
||||||
|
return "openai"
|
||||||
|
|
||||||
|
# ── 客户端初始化 ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _init_client(self) -> None:
|
||||||
|
"""初始化 OpenAI 客户端"""
|
||||||
|
if not self.cfg.api_key:
|
||||||
|
self.logger.warning(
|
||||||
|
"⚠️ LLM_API_KEY 未设置,OpenAI API 调用将失败。\n"
|
||||||
|
" 请设置环境变量: export LLM_API_KEY=sk-..."
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"api_key": self.cfg.api_key or "sk-placeholder",
|
||||||
|
"timeout": self.cfg.timeout,
|
||||||
|
"max_retries": self.cfg.max_retries,
|
||||||
|
}
|
||||||
|
if self.cfg.api_base_url:
|
||||||
|
kwargs["base_url"] = self.cfg.api_base_url
|
||||||
|
self.logger.info(f"🔗 使用自定义 API 地址: {self.cfg.api_base_url}")
|
||||||
|
|
||||||
|
self._client = OpenAI(**kwargs)
|
||||||
|
self.logger.info(
|
||||||
|
f"✅ OpenAI 客户端初始化完成\n"
|
||||||
|
f" model = {self.cfg.model_name}\n"
|
||||||
|
f" base_url = {self.cfg.api_base_url or 'https://api.openai.com/v1'}\n"
|
||||||
|
f" max_retries= {self.cfg.max_retries}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 核心接口实现
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def plan_with_tools(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
tool_schemas: list[ToolSchema],
|
||||||
|
) -> PlanResult:
|
||||||
|
"""
|
||||||
|
调用 OpenAI Function Calling 规划工具调用链
|
||||||
|
|
||||||
|
OpenAI 消息格式:
|
||||||
|
[
|
||||||
|
{"role": "system", "content": "..."},
|
||||||
|
{"role": "user", "content": "用户输入"},
|
||||||
|
]
|
||||||
|
|
||||||
|
OpenAI tools 格式:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "calculator",
|
||||||
|
"description": "计算数学表达式",
|
||||||
|
"parameters": {"type": "object", "properties": {...}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
返回 tool_calls 示例:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": "calculator", "arguments": '{"expression":"1+1"}'}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
if not self._client:
|
||||||
|
return PlanResult(plan=None, success=False, error="OpenAI 客户端未初始化")
|
||||||
|
|
||||||
|
# 构造 OpenAI tools 参数
|
||||||
|
tools = self._build_openai_tools(tool_schemas)
|
||||||
|
self.logger.debug(f"📤 发送规划请求,tools 数量: {len(tools)}")
|
||||||
|
self.logger.debug(f"📤 消息历史长度: {len(messages)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self._client.chat.completions.create(
|
||||||
|
model=self.cfg.model_name,
|
||||||
|
messages=messages,
|
||||||
|
tools=tools,
|
||||||
|
tool_choice="auto", # 由模型决定是否调用工具
|
||||||
|
temperature=self.cfg.temperature,
|
||||||
|
max_tokens=self.cfg.max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
usage = self._extract_usage(response)
|
||||||
|
self.logger.info(
|
||||||
|
f"📊 Token 用量: prompt={usage.get('prompt_tokens', 0)}, "
|
||||||
|
f"completion={usage.get('completion_tokens', 0)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 解析 tool_calls → ChainPlan
|
||||||
|
choice = response.choices[0]
|
||||||
|
message = choice.message
|
||||||
|
|
||||||
|
if not message.tool_calls:
|
||||||
|
# 模型决定不调用工具,直接回复
|
||||||
|
self.logger.info("💬 模型决策: 无需工具,直接回复")
|
||||||
|
return PlanResult(
|
||||||
|
plan=ChainPlan(goal="", steps=[]),
|
||||||
|
raw_response=response,
|
||||||
|
usage=usage,
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = self._parse_tool_calls(message.tool_calls)
|
||||||
|
self.logger.info(f"📋 解析到 {plan.step_count} 个工具调用步骤")
|
||||||
|
return PlanResult(
|
||||||
|
plan=plan,
|
||||||
|
raw_response=response,
|
||||||
|
usage=usage,
|
||||||
|
)
|
||||||
|
|
||||||
|
except AuthenticationError as e:
|
||||||
|
return self._handle_error("认证失败,请检查 API Key", e)
|
||||||
|
except RateLimitError as e:
|
||||||
|
return self._handle_error("请求频率超限,请稍后重试", e)
|
||||||
|
except APITimeoutError as e:
|
||||||
|
return self._handle_error(f"请求超时(>{self.cfg.timeout}s)", e)
|
||||||
|
except APIConnectionError as e:
|
||||||
|
return self._handle_error("网络连接失败,请检查网络或 api_base_url", e)
|
||||||
|
except APIStatusError as e:
|
||||||
|
return self._handle_error(f"API 错误 HTTP {e.status_code}: {e.message}", e)
|
||||||
|
except Exception as e:
|
||||||
|
return self._handle_error(f"未知错误: {e}", e)
|
||||||
|
|
||||||
|
def generate_reply(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
) -> ReplyResult:
|
||||||
|
"""
|
||||||
|
基于完整对话历史(含工具执行结果)生成最终自然语言回复
|
||||||
|
|
||||||
|
消息格式示例(含工具结果):
|
||||||
|
[
|
||||||
|
{"role": "system", "content": "..."},
|
||||||
|
{"role": "user", "content": "搜索天气然后计算..."},
|
||||||
|
{"role": "assistant", "content": None,
|
||||||
|
"tool_calls": [{"id":"call_1","function":{"name":"web_search",...}}]},
|
||||||
|
{"role": "tool", "content": "搜索结果...", "tool_call_id": "call_1"},
|
||||||
|
{"role": "assistant", "content": None,
|
||||||
|
"tool_calls": [{"id":"call_2","function":{"name":"calculator",...}}]},
|
||||||
|
{"role": "tool", "content": "计算结果: 312", "tool_call_id": "call_2"},
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
if not self._client:
|
||||||
|
return ReplyResult(content="", success=False, error="OpenAI 客户端未初始化")
|
||||||
|
|
||||||
|
self.logger.debug(f"📤 发送回复生成请求,消息长度: {len(messages)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self._client.chat.completions.create(
|
||||||
|
model=self.cfg.model_name,
|
||||||
|
messages=messages,
|
||||||
|
temperature=self.cfg.temperature,
|
||||||
|
max_tokens=self.cfg.max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
content = response.choices[0].message.content or ""
|
||||||
|
usage = self._extract_usage(response)
|
||||||
|
self.logger.info(
|
||||||
|
f"✅ 回复生成成功,长度: {len(content)} chars,"
|
||||||
|
f"Token: {usage.get('completion_tokens', 0)}"
|
||||||
|
)
|
||||||
|
return ReplyResult(content=content, usage=usage)
|
||||||
|
|
||||||
|
except AuthenticationError as e:
|
||||||
|
return ReplyResult(content="", success=False,
|
||||||
|
error=f"认证失败: {e}")
|
||||||
|
except RateLimitError as e:
|
||||||
|
return ReplyResult(content="", success=False,
|
||||||
|
error=f"频率超限: {e}")
|
||||||
|
except APITimeoutError as e:
|
||||||
|
return ReplyResult(content="", success=False,
|
||||||
|
error=f"请求超时: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
return ReplyResult(content="", success=False,
|
||||||
|
error=f"生成回复失败: {e}")
|
||||||
|
|
||||||
|
def health_check(self) -> bool:
|
||||||
|
"""发送最小请求检测 API 连通性"""
|
||||||
|
if not self._client:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self._client.chat.completions.create(
|
||||||
|
model=self.cfg.model_name,
|
||||||
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
|
max_tokens=1,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 工具方法
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_openai_tools(tool_schemas: list[ToolSchema]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
将 ToolSchema 列表转换为 OpenAI tools 参数格式
|
||||||
|
|
||||||
|
OpenAI 格式:
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "calculator",
|
||||||
|
"description": "计算数学表达式",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expression": {"type": "string", "description": "..."}
|
||||||
|
},
|
||||||
|
"required": ["expression"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
tools = []
|
||||||
|
for schema in tool_schemas:
|
||||||
|
tools.append({
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": schema.name,
|
||||||
|
"description": schema.description,
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": schema.parameters,
|
||||||
|
"required": list(schema.parameters.keys()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return tools
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_tool_calls(tool_calls: list) -> ChainPlan:
|
||||||
|
"""
|
||||||
|
将 OpenAI tool_calls 解析为 ChainPlan
|
||||||
|
|
||||||
|
OpenAI tool_calls 格式:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "calculator",
|
||||||
|
"arguments": '{"expression": "1+2"}' ← JSON 字符串
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
steps: list[ToolStep] = []
|
||||||
|
for idx, tc in enumerate(tool_calls):
|
||||||
|
fn = tc.function
|
||||||
|
tool_name = fn.name
|
||||||
|
try:
|
||||||
|
arguments = json.loads(fn.arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
arguments = {"raw": fn.arguments}
|
||||||
|
|
||||||
|
steps.append(ToolStep(
|
||||||
|
step_id=idx + 1,
|
||||||
|
tool_name=tool_name,
|
||||||
|
arguments=arguments,
|
||||||
|
description=f"调用 {tool_name}(由 OpenAI Function Calling 规划)",
|
||||||
|
depends_on=list(range(1, idx + 1)) if idx > 0 else [],
|
||||||
|
))
|
||||||
|
|
||||||
|
goal = " → ".join(s.tool_name for s in steps)
|
||||||
|
return ChainPlan(
|
||||||
|
goal=goal,
|
||||||
|
steps=steps,
|
||||||
|
is_single=len(steps) == 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_usage(response: Any) -> dict[str, int]:
|
||||||
|
"""提取 Token 用量信息"""
|
||||||
|
if hasattr(response, "usage") and response.usage:
|
||||||
|
return {
|
||||||
|
"prompt_tokens": response.usage.prompt_tokens,
|
||||||
|
"completion_tokens": response.usage.completion_tokens,
|
||||||
|
"total_tokens": response.usage.total_tokens,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _handle_error(self, msg: str, exc: Exception) -> PlanResult:
|
||||||
|
self.logger.error(f"❌ OpenAI API 错误: {msg}")
|
||||||
|
self.logger.debug(f" 原始异常: {exc}")
|
||||||
|
return PlanResult(plan=None, success=False, error=msg)
|
||||||
427
logs/agent.log
427
logs/agent.log
|
|
@ -1162,3 +1162,430 @@ The function `get_system_name()` uses `platform.system()` to determine the syste
|
||||||
[2026-03-02 14:27:44,134] [agent.MCP] INFO: 📨 收到请求 id=9118bd32 method=tools/call
|
[2026-03-02 14:27:44,134] [agent.MCP] INFO: 📨 收到请求 id=9118bd32 method=tools/call
|
||||||
[2026-03-02 14:27:44,134] [agent.TOOL] INFO: ▶ 执行工具 [tool_generator],参数: {'name': 'get_system_name', 'description': 'Returns the name of the operating system of the machine.', 'parameters': {}, 'code': 'import platform\ndef get_system_name():\n return platform.system()'}
|
[2026-03-02 14:27:44,134] [agent.TOOL] INFO: ▶ 执行工具 [tool_generator],参数: {'name': 'get_system_name', 'description': 'Returns the name of the operating system of the machine.', 'parameters': {}, 'code': 'import platform\ndef get_system_name():\n return platform.system()'}
|
||||||
[2026-03-02 14:27:44,134] [agent.TOOL] INFO: ✅ 工具 [tool_generator] 执行成功
|
[2026-03-02 14:27:44,134] [agent.TOOL] INFO: ✅ 工具 [tool_generator] 执行成功
|
||||||
|
[2026-03-09 13:11:25,019] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(配置文件驱动)...
|
||||||
|
[2026-03-09 13:11:25,019] [agent.SYSTEM] INFO: ──────────────────────────────────────────────────
|
||||||
|
📋 当前配置
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[LLM] provider = anthropic
|
||||||
|
[LLM] model_name = claude-sonnet-4-6
|
||||||
|
[LLM] model_path = (未设置)
|
||||||
|
[LLM] api_key = (未设置)
|
||||||
|
[LLM] temperature = 0.7
|
||||||
|
[MCP] server_name = DemoMCPServer
|
||||||
|
[MCP] transport = stdio
|
||||||
|
[MCP] tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[MEMORY] max_history = 20
|
||||||
|
[AGENT] multi_step = True
|
||||||
|
[AGENT] max_steps = 10
|
||||||
|
[LOG] level = DEBUG
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:11:25,020] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
|
||||||
|
[2026-03-09 13:11:25,020] [agent.MCP] INFO: transport = stdio
|
||||||
|
[2026-03-09 13:11:25,020] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:11:25,021] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10
|
||||||
|
[2026-03-09 13:11:25,021] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
|
||||||
|
[2026-03-09 13:11:25,021] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置)
|
||||||
|
[2026-03-09 13:11:25,022] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
|
||||||
|
[2026-03-09 13:11:25,022] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB
|
||||||
|
[2026-03-09 13:11:25,022] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录
|
||||||
|
[2026-03-09 13:11:25,022] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True
|
||||||
|
[2026-03-09 13:11:25,022] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
|
||||||
|
[2026-03-09 13:11:25,023] [agent.LLM] INFO: 🧠 LLM 引擎初始化
|
||||||
|
[2026-03-09 13:11:25,024] [agent.LLM] INFO: provider = anthropic
|
||||||
|
[2026-03-09 13:11:25,024] [agent.LLM] INFO: model_name = claude-sonnet-4-6
|
||||||
|
[2026-03-09 13:11:25,024] [agent.LLM] INFO: model_path = (未设置)
|
||||||
|
[2026-03-09 13:11:25,025] [agent.LLM] INFO: api_base = (默认)
|
||||||
|
[2026-03-09 13:11:25,025] [agent.LLM] INFO: temperature= 0.7
|
||||||
|
[2026-03-09 13:11:25,025] [agent.LLM] INFO: max_tokens = 4096
|
||||||
|
[2026-03-09 13:11:25,026] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
|
||||||
|
[2026-03-09 13:11:25,027] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(支持多步串行调用)
|
||||||
|
[2026-03-09 13:11:25,027] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:14:47,765] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(配置文件驱动)...
|
||||||
|
[2026-03-09 13:14:47,768] [agent.SYSTEM] INFO: ──────────────────────────────────────────────────
|
||||||
|
📋 当前配置
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[LLM] provider = anthropic
|
||||||
|
[LLM] model_name = claude-sonnet-4-6
|
||||||
|
[LLM] model_path = (未设置)
|
||||||
|
[LLM] api_key = (未设置)
|
||||||
|
[LLM] temperature = 0.7
|
||||||
|
[MCP] server_name = DemoMCPServer
|
||||||
|
[MCP] transport = stdio
|
||||||
|
[MCP] tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[MEMORY] max_history = 20
|
||||||
|
[AGENT] multi_step = True
|
||||||
|
[AGENT] max_steps = 10
|
||||||
|
[LOG] level = DEBUG
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:14:47,769] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
|
||||||
|
[2026-03-09 13:14:47,769] [agent.MCP] INFO: transport = stdio
|
||||||
|
[2026-03-09 13:14:47,769] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:14:47,770] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10
|
||||||
|
[2026-03-09 13:14:47,770] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
|
||||||
|
[2026-03-09 13:14:47,770] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置)
|
||||||
|
[2026-03-09 13:14:47,770] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
|
||||||
|
[2026-03-09 13:14:47,771] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB
|
||||||
|
[2026-03-09 13:14:47,771] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录
|
||||||
|
[2026-03-09 13:14:47,771] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True
|
||||||
|
[2026-03-09 13:14:47,771] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
|
||||||
|
[2026-03-09 13:14:47,772] [agent.LLM] INFO: 🧠 LLM 引擎初始化
|
||||||
|
[2026-03-09 13:14:47,773] [agent.LLM] INFO: provider = anthropic
|
||||||
|
[2026-03-09 13:14:47,773] [agent.LLM] INFO: model_name = claude-sonnet-4-6
|
||||||
|
[2026-03-09 13:14:47,773] [agent.LLM] INFO: model_path = (未设置)
|
||||||
|
[2026-03-09 13:14:47,773] [agent.LLM] INFO: api_base = (默认)
|
||||||
|
[2026-03-09 13:14:47,773] [agent.LLM] INFO: temperature= 0.7
|
||||||
|
[2026-03-09 13:14:47,774] [agent.LLM] INFO: max_tokens = 4096
|
||||||
|
[2026-03-09 13:14:47,775] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
|
||||||
|
[2026-03-09 13:14:47,776] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(支持多步串行调用)
|
||||||
|
[2026-03-09 13:14:47,777] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:15:29,737] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(配置文件驱动)...
|
||||||
|
[2026-03-09 13:15:29,739] [agent.SYSTEM] INFO: ──────────────────────────────────────────────────
|
||||||
|
📋 当前配置
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[LLM] provider = monica
|
||||||
|
[LLM] model_name = gpt-4o
|
||||||
|
[LLM] model_path = (未设置)
|
||||||
|
[LLM] api_key = ***
|
||||||
|
[LLM] temperature = 0.7
|
||||||
|
[MCP] server_name = MCPServer
|
||||||
|
[MCP] transport = stdio
|
||||||
|
[MCP] tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[MEMORY] max_history = 20
|
||||||
|
[AGENT] multi_step = True
|
||||||
|
[AGENT] max_steps = 10
|
||||||
|
[LOG] level = DEBUG
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:15:29,742] [agent.MCP] INFO: 🚀 MCP Server [MCPServer] 启动
|
||||||
|
[2026-03-09 13:15:29,743] [agent.MCP] INFO: transport = stdio
|
||||||
|
[2026-03-09 13:15:29,743] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:15:29,745] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10
|
||||||
|
[2026-03-09 13:15:29,745] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
|
||||||
|
[2026-03-09 13:15:29,745] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置)
|
||||||
|
[2026-03-09 13:15:29,745] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
|
||||||
|
[2026-03-09 13:15:29,746] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB
|
||||||
|
[2026-03-09 13:15:29,746] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录
|
||||||
|
[2026-03-09 13:15:29,747] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True
|
||||||
|
[2026-03-09 13:15:29,748] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
|
||||||
|
[2026-03-09 13:15:29,748] [agent.LLM] INFO: 🧠 LLM 引擎初始化
|
||||||
|
[2026-03-09 13:15:29,749] [agent.LLM] INFO: provider = monica
|
||||||
|
[2026-03-09 13:15:29,749] [agent.LLM] INFO: model_name = gpt-4o
|
||||||
|
[2026-03-09 13:15:29,749] [agent.LLM] INFO: model_path = (未设置)
|
||||||
|
[2026-03-09 13:15:29,749] [agent.LLM] INFO: api_base = https://openapi.monica.im/v1
|
||||||
|
[2026-03-09 13:15:29,750] [agent.LLM] INFO: temperature= 0.7
|
||||||
|
[2026-03-09 13:15:29,750] [agent.LLM] INFO: max_tokens = 4096
|
||||||
|
[2026-03-09 13:15:29,751] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
|
||||||
|
[2026-03-09 13:15:29,752] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(支持多步串行调用)
|
||||||
|
[2026-03-09 13:15:29,752] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:17:27,993] [agent.CLIENT] INFO: ══════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:17:27,995] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 从从网络搜索获取系统名称的python代码并进行执行
|
||||||
|
[2026-03-09 13:17:27,996] [agent.CLIENT] INFO: ══════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:17:27,996] [agent.MEMORY] DEBUG: 💬 [USER] 从从网络搜索获取系统名称的python代码并进行执行...
|
||||||
|
[2026-03-09 13:17:27,997] [agent.CLIENT] INFO: 🗺 Step 2 [LLM] 分析任务,规划工具调用链...
|
||||||
|
[2026-03-09 13:17:27,997] [agent.LLM] INFO: 🗺 规划工具调用链 [gpt-4o]: 从从网络搜索获取系统名称的python代码并进行执行...
|
||||||
|
[2026-03-09 13:17:27,998] [agent.LLM] DEBUG: 📝 Prompt 已构造 (436 chars)
|
||||||
|
[2026-03-09 13:17:27,998] [agent.LLM] INFO: 📋 规划完成: ChainPlan(goal='从从网络搜索获取系统名称的python代码并进行执行', chain=[1]web_search)
|
||||||
|
[2026-03-09 13:17:27,998] [agent.LLM] INFO: Step 1: [web_search] depends_on=[] — 网络搜索
|
||||||
|
[2026-03-09 13:17:27,998] [agent.CLIENT] INFO:
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
🔗 开始执行工具调用链
|
||||||
|
目标: 从从网络搜索获取系统名称的python代码并进行执行
|
||||||
|
步骤: 1 步
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:17:27,999] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 1/1 执行中
|
||||||
|
工具: [web_search]
|
||||||
|
说明: 网络搜索
|
||||||
|
参数: {'query': '从从网络搜索获取系统名称的python代码并进行执行', 'max_results': 5}
|
||||||
|
[2026-03-09 13:17:28,002] [agent.CLIENT] INFO: 📡 [MCP] 发送请求: {'jsonrpc': '2.0', 'id': 'dc801fda', 'method': 'tools/call', 'params': {'name': 'web_search', 'arguments': {'query': '从从网络搜索获取系统名称的python代码并进行执行', 'max_results': 5}}}
|
||||||
|
[2026-03-09 13:17:28,003] [agent.MCP] INFO: 📨 收到请求 id=dc801fda method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:17:28,003] [agent.TOOL] INFO: ▶ 执行工具 [web_search],参数: {'query': '从从网络搜索获取系统名称的python代码并进行执行', 'max_results': 5}
|
||||||
|
[2026-03-09 13:17:28,108] [agent.TOOL] INFO: ✅ 工具 [web_search] 执行成功
|
||||||
|
[2026-03-09 13:17:28,109] [agent.CLIENT] INFO: ✅ Step 1 成功: 搜索「从从网络搜索获取系统名称的python代码并进行执行」(mock),共 2 条:
|
||||||
|
[1] Python 官方文档
|
||||||
|
Python 3.12 新特性:...
|
||||||
|
[2026-03-09 13:17:28,109] [agent.MEMORY] DEBUG: 💬 [TOOL] 搜索「从从网络搜索获取系统名称的python代码并进行执行」(mock),共 2 条:
|
||||||
|
[1] Python 官方文档
|
||||||
|
...
|
||||||
|
[2026-03-09 13:17:28,111] [agent.CLIENT] INFO: ──────────────────────────────────────────────────────────
|
||||||
|
✅ 调用链执行完成
|
||||||
|
完成: 1/1 步
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:17:28,111] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 整合所有步骤结果,生成最终回复...
|
||||||
|
[2026-03-09 13:17:28,111] [agent.LLM] INFO: ✍️ 整合单步工具结果 [gpt-4o]...
|
||||||
|
[2026-03-09 13:17:28,111] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ 已通过 [web_search] 工具处理您的请求。
|
||||||
|
|
||||||
|
**执行结果:**
|
||||||
|
搜索「从从网络搜索获取系统名称的pyth...
|
||||||
|
[2026-03-09 13:18:21,524] [agent.CLIENT] INFO: ══════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:18:21,524] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 计算1+1等于多少
|
||||||
|
[2026-03-09 13:18:21,524] [agent.CLIENT] INFO: ══════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:18:21,525] [agent.MEMORY] DEBUG: 💬 [USER] 计算1+1等于多少...
|
||||||
|
[2026-03-09 13:18:21,525] [agent.CLIENT] INFO: 🗺 Step 2 [LLM] 分析任务,规划工具调用链...
|
||||||
|
[2026-03-09 13:18:21,526] [agent.LLM] INFO: 🗺 规划工具调用链 [gpt-4o]: 计算1+1等于多少...
|
||||||
|
[2026-03-09 13:18:21,526] [agent.LLM] DEBUG: 📝 Prompt 已构造 (617 chars)
|
||||||
|
[2026-03-09 13:18:21,528] [agent.LLM] INFO: 📋 规划完成: ChainPlan(goal='计算1+1等于多少', chain=[1]calculator)
|
||||||
|
[2026-03-09 13:18:21,528] [agent.LLM] INFO: Step 1: [calculator] depends_on=[] — 数学计算
|
||||||
|
[2026-03-09 13:18:21,529] [agent.CLIENT] INFO:
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
🔗 开始执行工具调用链
|
||||||
|
目标: 计算1+1等于多少
|
||||||
|
步骤: 1 步
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:18:21,529] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 1/1 执行中
|
||||||
|
工具: [calculator]
|
||||||
|
说明: 数学计算
|
||||||
|
参数: {'expression': '1+1'}
|
||||||
|
[2026-03-09 13:18:21,530] [agent.CLIENT] INFO: 📡 [MCP] 发送请求: {'jsonrpc': '2.0', 'id': 'd6becfe5', 'method': 'tools/call', 'params': {'name': 'calculator', 'arguments': {'expression': '1+1'}}}
|
||||||
|
[2026-03-09 13:18:21,530] [agent.MCP] INFO: 📨 收到请求 id=d6becfe5 method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:18:21,530] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '1+1'}
|
||||||
|
[2026-03-09 13:18:21,532] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
|
||||||
|
[2026-03-09 13:18:21,533] [agent.CLIENT] INFO: ✅ Step 1 成功: 1+1 = 2...
|
||||||
|
[2026-03-09 13:18:21,533] [agent.MEMORY] DEBUG: 💬 [TOOL] 1+1 = 2...
|
||||||
|
[2026-03-09 13:18:21,533] [agent.CLIENT] INFO: ──────────────────────────────────────────────────────────
|
||||||
|
✅ 调用链执行完成
|
||||||
|
完成: 1/1 步
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:18:21,534] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 整合所有步骤结果,生成最终回复...
|
||||||
|
[2026-03-09 13:18:21,534] [agent.LLM] INFO: ✍️ 整合单步工具结果 [gpt-4o]...
|
||||||
|
[2026-03-09 13:18:21,534] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ 已通过 [calculator] 工具处理您的请求。
|
||||||
|
|
||||||
|
**执行结果:**
|
||||||
|
1+1 = 2
|
||||||
|
|
||||||
|
---
|
||||||
|
*由 gpt-...
|
||||||
|
[2026-03-09 13:29:29,657] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)...
|
||||||
|
[2026-03-09 13:29:29,659] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────
|
||||||
|
📋 当前配置
|
||||||
|
────────────────────────────────────────────────────
|
||||||
|
[LLM] provider = openai
|
||||||
|
[LLM] model_name = gpt-4o
|
||||||
|
[LLM] api_key = (未设置)
|
||||||
|
[LLM] api_base_url = (默认)
|
||||||
|
[LLM] temperature = 0.7
|
||||||
|
[LLM] max_tokens = 4096
|
||||||
|
[LLM] function_calling = True
|
||||||
|
[LLM] stream = False
|
||||||
|
[LLM] max_retries = 3
|
||||||
|
[MCP] server_name = DemoMCPServer
|
||||||
|
[MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[MEMORY] max_history = 20
|
||||||
|
[AGENT] multi_step = True
|
||||||
|
[AGENT] fallback_rules = True
|
||||||
|
[LOG] level = DEBUG
|
||||||
|
────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:29:29,661] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
|
||||||
|
[2026-03-09 13:29:29,662] [agent.MCP] INFO: transport = stdio
|
||||||
|
[2026-03-09 13:29:29,662] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:29:29,663] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10
|
||||||
|
[2026-03-09 13:29:29,663] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
|
||||||
|
[2026-03-09 13:29:29,664] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置)
|
||||||
|
[2026-03-09 13:29:29,664] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
|
||||||
|
[2026-03-09 13:29:29,664] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB
|
||||||
|
[2026-03-09 13:29:29,664] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录
|
||||||
|
[2026-03-09 13:29:29,665] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True
|
||||||
|
[2026-03-09 13:29:29,665] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
|
||||||
|
[2026-03-09 13:29:29,665] [agent.LLM] INFO: 🏭 Provider 工厂: 创建 [openai] Provider
|
||||||
|
[2026-03-09 13:29:33,124] [agent.LLM] WARNING: ⚠️ LLM_API_KEY 未设置,OpenAI API 调用将失败。
|
||||||
|
请设置环境变量: export LLM_API_KEY=sk-...
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: ✅ OpenAI 客户端初始化完成
|
||||||
|
model = gpt-4o
|
||||||
|
base_url = https://api.openai.com/v1
|
||||||
|
max_retries= 3
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: 🧠 LLM 引擎初始化完成
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: provider = openai
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: model_name = gpt-4o
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: function_calling = True
|
||||||
|
[2026-03-09 13:29:33,315] [agent.LLM] INFO: temperature = 0.7
|
||||||
|
[2026-03-09 13:29:33,316] [agent.LLM] INFO: fallback_rules = True
|
||||||
|
[2026-03-09 13:29:33,316] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
|
||||||
|
[2026-03-09 13:29:33,317] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(OpenAI Function Calling 模式)
|
||||||
|
[2026-03-09 13:29:33,317] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:30:14,954] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)...
|
||||||
|
[2026-03-09 13:30:14,954] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────
|
||||||
|
📋 当前配置
|
||||||
|
────────────────────────────────────────────────────
|
||||||
|
[LLM] provider = openai
|
||||||
|
[LLM] model_name = gpt-4o
|
||||||
|
[LLM] api_key = ***
|
||||||
|
[LLM] api_base_url = https://openapi.monica.im/v1
|
||||||
|
[LLM] temperature = 0.7
|
||||||
|
[LLM] max_tokens = 4096
|
||||||
|
[LLM] function_calling = True
|
||||||
|
[LLM] stream = False
|
||||||
|
[LLM] max_retries = 3
|
||||||
|
[MCP] server_name = DemoMCPServer
|
||||||
|
[MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[MEMORY] max_history = 20
|
||||||
|
[AGENT] multi_step = True
|
||||||
|
[AGENT] fallback_rules = True
|
||||||
|
[LOG] level = DEBUG
|
||||||
|
────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:30:14,955] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
|
||||||
|
[2026-03-09 13:30:14,956] [agent.MCP] INFO: transport = stdio
|
||||||
|
[2026-03-09 13:30:14,956] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:30:14,957] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10
|
||||||
|
[2026-03-09 13:30:14,957] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
|
||||||
|
[2026-03-09 13:30:14,957] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置)
|
||||||
|
[2026-03-09 13:30:14,958] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
|
||||||
|
[2026-03-09 13:30:14,958] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB
|
||||||
|
[2026-03-09 13:30:14,959] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录
|
||||||
|
[2026-03-09 13:30:14,960] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True
|
||||||
|
[2026-03-09 13:30:14,960] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
|
||||||
|
[2026-03-09 13:30:14,961] [agent.LLM] INFO: 🏭 Provider 工厂: 创建 [openai] Provider
|
||||||
|
[2026-03-09 13:30:17,075] [agent.LLM] INFO: 🔗 使用自定义 API 地址: https://openapi.monica.im/v1
|
||||||
|
[2026-03-09 13:30:17,245] [agent.LLM] INFO: ✅ OpenAI 客户端初始化完成
|
||||||
|
model = gpt-4o
|
||||||
|
base_url = https://openapi.monica.im/v1
|
||||||
|
max_retries= 3
|
||||||
|
[2026-03-09 13:30:17,246] [agent.LLM] INFO: 🧠 LLM 引擎初始化完成
|
||||||
|
[2026-03-09 13:30:17,246] [agent.LLM] INFO: provider = openai
|
||||||
|
[2026-03-09 13:30:17,246] [agent.LLM] INFO: model_name = gpt-4o
|
||||||
|
[2026-03-09 13:30:17,246] [agent.LLM] INFO: function_calling = True
|
||||||
|
[2026-03-09 13:30:17,247] [agent.LLM] INFO: temperature = 0.7
|
||||||
|
[2026-03-09 13:30:17,247] [agent.LLM] INFO: fallback_rules = True
|
||||||
|
[2026-03-09 13:30:17,248] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
|
||||||
|
[2026-03-09 13:30:17,249] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(OpenAI Function Calling 模式)
|
||||||
|
[2026-03-09 13:30:17,249] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
|
||||||
|
[2026-03-09 13:30:39,084] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:30:39,085] [agent.CLIENT] INFO: 📨 收到用户输入: 234乘以123等于多少
|
||||||
|
[2026-03-09 13:30:39,085] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:30:39,086] [agent.MEMORY] DEBUG: 💬 [USER] 234乘以123等于多少...
|
||||||
|
[2026-03-09 13:30:39,086] [agent.CLIENT] INFO: 🗺 Step 2 [LLM] 规划工具调用链...
|
||||||
|
[2026-03-09 13:30:39,086] [agent.LLM] INFO: 🗺 规划工具调用链: 234乘以123等于多少...
|
||||||
|
[2026-03-09 13:30:39,087] [agent.LLM] DEBUG: 📤 发送规划请求,tools 数量: 4
|
||||||
|
[2026-03-09 13:30:39,087] [agent.LLM] DEBUG: 📤 消息历史长度: 3
|
||||||
|
[2026-03-09 13:30:42,294] [agent.LLM] INFO: 📊 Token 用量: prompt=395, completion=17
|
||||||
|
[2026-03-09 13:30:42,294] [agent.LLM] INFO: 📋 解析到 1 个工具调用步骤
|
||||||
|
[2026-03-09 13:30:42,294] [agent.LLM] INFO: 📋 OpenAI 规划完成: 1 步
|
||||||
|
[2026-03-09 13:30:42,294] [agent.LLM] INFO: Step 1: [calculator] args={'expression': '234*123'}
|
||||||
|
[2026-03-09 13:30:42,294] [agent.CLIENT] INFO:
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
🔗 开始执行工具调用链
|
||||||
|
目标: calculator
|
||||||
|
步骤: 1 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:30:42,295] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 1 执行中
|
||||||
|
工具: [calculator]
|
||||||
|
说明: 调用 calculator(由 OpenAI Function Calling 规划)
|
||||||
|
参数: {'expression': '234*123'}
|
||||||
|
call_id: call_6b8b5745
|
||||||
|
[2026-03-09 13:30:42,295] [agent.MCP] INFO: 📨 收到请求 id=11862afa method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:30:42,295] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '234*123'}
|
||||||
|
[2026-03-09 13:30:42,297] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
|
||||||
|
[2026-03-09 13:30:42,297] [agent.CLIENT] INFO: ✅ Step 1 成功: 234*123 = 28782...
|
||||||
|
[2026-03-09 13:30:42,297] [agent.MEMORY] DEBUG: 💬 [TOOL] 234*123 = 28782...
|
||||||
|
[2026-03-09 13:30:42,297] [agent.CLIENT] INFO: ────────────────────────────────────────────────────────────
|
||||||
|
✅ 调用链执行完成
|
||||||
|
完成: 1/1 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:30:42,297] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...
|
||||||
|
[2026-03-09 13:30:42,298] [agent.LLM] INFO: ✍️ 整合单步工具结果 [calculator]...
|
||||||
|
[2026-03-09 13:30:42,298] [agent.LLM] INFO: ✍️ 生成最终回复...
|
||||||
|
[2026-03-09 13:30:42,298] [agent.LLM] DEBUG: 📤 发送回复生成请求,消息长度: 3
|
||||||
|
[2026-03-09 13:30:42,924] [agent.LLM] WARNING: ⚠️ OpenAI 回复生成失败: 生成回复失败: Error code: 400 - {'error': {'code': '11111', 'message': "StatusCode: 400: API returned unexpected status code: 400: Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.", 'type': 'invalid_request_error'}}
|
||||||
|
[2026-03-09 13:30:42,924] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ **任务已完成**
|
||||||
|
|
||||||
|
针对您的需求「234乘以123等于多少」,执行结果如下:
|
||||||
|
|
||||||
|
234*123 = 28782...
|
||||||
|
[2026-03-09 13:30:42,924] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回
|
||||||
|
[2026-03-09 13:34:35,843] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:34:35,845] [agent.CLIENT] INFO: 📨 收到用户输入: 先计算34乘以45,再计算12乘以23
|
||||||
|
[2026-03-09 13:34:35,845] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:34:35,846] [agent.MEMORY] DEBUG: 💬 [USER] 先计算34乘以45,再计算12乘以23...
|
||||||
|
[2026-03-09 13:34:35,846] [agent.CLIENT] INFO: 🗺 Step 2 [LLM] 规划工具调用链...
|
||||||
|
[2026-03-09 13:34:35,846] [agent.LLM] INFO: 🗺 规划工具调用链: 先计算34乘以45,再计算12乘以23...
|
||||||
|
[2026-03-09 13:34:35,846] [agent.LLM] DEBUG: 📤 发送规划请求,tools 数量: 4
|
||||||
|
[2026-03-09 13:34:35,847] [agent.LLM] DEBUG: 📤 消息历史长度: 4
|
||||||
|
[2026-03-09 13:34:37,936] [agent.LLM] INFO: 📊 Token 用量: prompt=424, completion=49
|
||||||
|
[2026-03-09 13:34:37,936] [agent.LLM] INFO: 📋 解析到 2 个工具调用步骤
|
||||||
|
[2026-03-09 13:34:37,937] [agent.LLM] INFO: 📋 OpenAI 规划完成: 2 步
|
||||||
|
[2026-03-09 13:34:37,937] [agent.LLM] INFO: Step 1: [calculator] args={'expression': '34*45'}
|
||||||
|
[2026-03-09 13:34:37,937] [agent.LLM] INFO: Step 2: [calculator] args={'expression': '12*23'}
|
||||||
|
[2026-03-09 13:34:37,937] [agent.CLIENT] INFO:
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
🔗 开始执行工具调用链
|
||||||
|
目标: calculator → calculator
|
||||||
|
步骤: 2 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:34:37,938] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 1 执行中
|
||||||
|
工具: [calculator]
|
||||||
|
说明: 调用 calculator(由 OpenAI Function Calling 规划)
|
||||||
|
参数: {'expression': '34*45'}
|
||||||
|
call_id: call_78edf968
|
||||||
|
[2026-03-09 13:34:37,938] [agent.MCP] INFO: 📨 收到请求 id=ec8a2d23 method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:34:37,938] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '34*45'}
|
||||||
|
[2026-03-09 13:34:37,938] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
|
||||||
|
[2026-03-09 13:34:37,938] [agent.CLIENT] INFO: ✅ Step 1 成功: 34*45 = 1530...
|
||||||
|
[2026-03-09 13:34:37,938] [agent.MEMORY] DEBUG: 💬 [TOOL] 34*45 = 1530...
|
||||||
|
[2026-03-09 13:34:37,939] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 2 执行中
|
||||||
|
工具: [calculator]
|
||||||
|
说明: 调用 calculator(由 OpenAI Function Calling 规划)
|
||||||
|
参数: {'expression': '12*23'}
|
||||||
|
call_id: call_f3174090
|
||||||
|
[2026-03-09 13:34:37,939] [agent.MCP] INFO: 📨 收到请求 id=ce5f9e24 method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:34:37,939] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '12*23'}
|
||||||
|
[2026-03-09 13:34:37,939] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
|
||||||
|
[2026-03-09 13:34:37,939] [agent.CLIENT] INFO: ✅ Step 2 成功: 12*23 = 276...
|
||||||
|
[2026-03-09 13:34:37,940] [agent.MEMORY] DEBUG: 💬 [TOOL] 12*23 = 276...
|
||||||
|
[2026-03-09 13:34:37,940] [agent.CLIENT] INFO: ────────────────────────────────────────────────────────────
|
||||||
|
✅ 调用链执行完成
|
||||||
|
完成: 2/2 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:34:37,940] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...
|
||||||
|
[2026-03-09 13:34:37,940] [agent.LLM] INFO: ✍️ 生成最终回复...
|
||||||
|
[2026-03-09 13:34:37,940] [agent.LLM] DEBUG: 📤 发送回复生成请求,消息长度: 5
|
||||||
|
[2026-03-09 13:34:39,468] [agent.LLM] INFO: ✅ 回复生成成功,长度: 25 chars,Token: 18
|
||||||
|
[2026-03-09 13:34:39,469] [agent.LLM] INFO: ✅ OpenAI 回复生成成功 (25 chars)
|
||||||
|
[2026-03-09 13:34:39,469] [agent.MEMORY] DEBUG: 💬 [CHAIN] 34乘以45等于1530,12乘以23等于276。...
|
||||||
|
[2026-03-09 13:34:39,469] [agent.MEMORY] INFO: 🔗 调用链已记录: 2/2 步成功
|
||||||
|
[2026-03-09 13:34:39,470] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回
|
||||||
|
[2026-03-09 13:35:13,266] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:35:13,267] [agent.CLIENT] INFO: 📨 收到用户输入: 先计算34乘以56,再将结果乘以12
|
||||||
|
[2026-03-09 13:35:13,268] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════
|
||||||
|
[2026-03-09 13:35:13,268] [agent.MEMORY] DEBUG: 💬 [USER] 先计算34乘以56,再将结果乘以12...
|
||||||
|
[2026-03-09 13:35:13,269] [agent.CLIENT] INFO: 🗺 Step 2 [LLM] 规划工具调用链...
|
||||||
|
[2026-03-09 13:35:13,269] [agent.LLM] INFO: 🗺 规划工具调用链: 先计算34乘以56,再将结果乘以12...
|
||||||
|
[2026-03-09 13:35:13,269] [agent.LLM] DEBUG: 📤 发送规划请求,tools 数量: 4
|
||||||
|
[2026-03-09 13:35:13,270] [agent.LLM] DEBUG: 📤 消息历史长度: 6
|
||||||
|
[2026-03-09 13:35:15,298] [agent.LLM] INFO: 📊 Token 用量: prompt=461, completion=17
|
||||||
|
[2026-03-09 13:35:15,299] [agent.LLM] INFO: 📋 解析到 1 个工具调用步骤
|
||||||
|
[2026-03-09 13:35:15,299] [agent.LLM] INFO: 📋 OpenAI 规划完成: 1 步
|
||||||
|
[2026-03-09 13:35:15,299] [agent.LLM] INFO: Step 1: [calculator] args={'expression': '34*56'}
|
||||||
|
[2026-03-09 13:35:15,299] [agent.CLIENT] INFO:
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
🔗 开始执行工具调用链
|
||||||
|
目标: calculator
|
||||||
|
步骤: 1 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:35:15,300] [agent.CLIENT] INFO:
|
||||||
|
▶ Step 1 执行中
|
||||||
|
工具: [calculator]
|
||||||
|
说明: 调用 calculator(由 OpenAI Function Calling 规划)
|
||||||
|
参数: {'expression': '34*56'}
|
||||||
|
call_id: call_6e1cc584
|
||||||
|
[2026-03-09 13:35:15,300] [agent.MCP] INFO: 📨 收到请求 id=a2b93b20 method=tools/call transport=stdio
|
||||||
|
[2026-03-09 13:35:15,300] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '34*56'}
|
||||||
|
[2026-03-09 13:35:15,300] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
|
||||||
|
[2026-03-09 13:35:15,301] [agent.CLIENT] INFO: ✅ Step 1 成功: 34*56 = 1904...
|
||||||
|
[2026-03-09 13:35:15,301] [agent.MEMORY] DEBUG: 💬 [TOOL] 34*56 = 1904...
|
||||||
|
[2026-03-09 13:35:15,301] [agent.CLIENT] INFO: ────────────────────────────────────────────────────────────
|
||||||
|
✅ 调用链执行完成
|
||||||
|
完成: 1/1 步
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
[2026-03-09 13:35:15,301] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 调用 OpenAI 生成最终回复...
|
||||||
|
[2026-03-09 13:35:15,301] [agent.LLM] INFO: ✍️ 整合单步工具结果 [calculator]...
|
||||||
|
[2026-03-09 13:35:15,302] [agent.LLM] INFO: ✍️ 生成最终回复...
|
||||||
|
[2026-03-09 13:35:15,302] [agent.LLM] DEBUG: 📤 发送回复生成请求,消息长度: 3
|
||||||
|
[2026-03-09 13:35:15,929] [agent.LLM] WARNING: ⚠️ OpenAI 回复生成失败: 生成回复失败: Error code: 400 - {'error': {'code': '11111', 'message': "StatusCode: 400: API returned unexpected status code: 400: Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.", 'type': 'invalid_request_error'}}
|
||||||
|
[2026-03-09 13:35:15,929] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ **任务已完成**
|
||||||
|
|
||||||
|
针对您的需求「先计算34乘以56,再将结果乘以12」,执行结果如下:
|
||||||
|
|
||||||
|
34*56 = 190...
|
||||||
|
[2026-03-09 13:35:15,930] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回
|
||||||
|
|
|
||||||
281
main.py
281
main.py
|
|
@ -1,14 +1,21 @@
|
||||||
"""程序入口"""
|
|
||||||
"""
|
"""
|
||||||
main.py
|
main.py
|
||||||
智能体 Demo 程序入口
|
智能体 Demo 程序入口(OpenAI Function Calling 驱动)
|
||||||
组装所有模块,启动交互式对话循环
|
|
||||||
|
运行模式:
|
||||||
|
python main.py → 交互模式
|
||||||
|
python main.py demo → 演示模式
|
||||||
|
python main.py config → 打印当前配置
|
||||||
|
python main.py health → 检测 OpenAI API 连通性
|
||||||
|
LLM_API_KEY=sk-xxx python main.py → 指定 API Key
|
||||||
|
LLM_MODEL_NAME=gpt-4-turbo python main.py→ 指定模型
|
||||||
|
AGENT_CONFIG_PATH=my.yaml python main.py → 指定配置文件
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
|
||||||
# ── 导入各模块 ─────────────────────────────────────────────────
|
from client.agent_client import AgentClient, AgentResponse
|
||||||
from client.agent_client import AgentClient
|
from config.settings import settings
|
||||||
from llm.llm_engine import LLMEngine
|
from llm.llm_engine import LLMEngine
|
||||||
from mcp.mcp_server import MCPServer
|
from mcp.mcp_server import MCPServer
|
||||||
from memory.memory_store import MemoryStore
|
from memory.memory_store import MemoryStore
|
||||||
|
|
@ -16,92 +23,142 @@ from tools.calculator import CalculatorTool
|
||||||
from tools.code_executor import CodeExecutorTool
|
from tools.code_executor import CodeExecutorTool
|
||||||
from tools.file_reader import FileReaderTool
|
from tools.file_reader import FileReaderTool
|
||||||
from tools.web_search import WebSearchTool
|
from tools.web_search import WebSearchTool
|
||||||
from tools.tool_generator import ToolGeneratorTool
|
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger("SYSTEM")
|
logger = get_logger("SYSTEM")
|
||||||
|
|
||||||
|
_ALL_TOOLS = {
|
||||||
|
"calculator": CalculatorTool,
|
||||||
|
"web_search": WebSearchTool,
|
||||||
|
"file_reader": FileReaderTool,
|
||||||
|
"code_executor": CodeExecutorTool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── 系统组装 ───────────────────────────────────────────────────
|
# ── 系统组装 ───────────────────────────────────────────────────
|
||||||
def build_agent(agent_prompt) -> AgentClient:
|
def build_agent() -> AgentClient:
|
||||||
"""
|
"""工厂函数:由 settings 驱动的 Agent 组装"""
|
||||||
工厂函数:组装并返回完整的 Agent 实例
|
logger.info("🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)...")
|
||||||
|
logger.info(settings.display())
|
||||||
|
|
||||||
组装顺序:
|
mcp_server = MCPServer()
|
||||||
1. 初始化 MCP Server,注册所有工具
|
for tool_cls in _ALL_TOOLS.values():
|
||||||
2. 初始化 LLM 引擎
|
mcp_server.register_tool(tool_cls)
|
||||||
3. 初始化 Memory 模块
|
|
||||||
4. 组装 AgentClient
|
|
||||||
"""
|
|
||||||
logger.info("🔧 开始组装 Agent 系统...")
|
|
||||||
|
|
||||||
# 1. MCP Server:注册所有工具
|
llm = LLMEngine()
|
||||||
mcp_server = MCPServer(server_name="DemoMCPServer")
|
memory = MemoryStore(max_history=settings.memory.max_history)
|
||||||
mcp_server.register_tools(
|
client = AgentClient(llm=llm, mcp_server=mcp_server, memory=memory)
|
||||||
CalculatorTool,
|
|
||||||
WebSearchTool,
|
|
||||||
FileReaderTool,
|
|
||||||
CodeExecutorTool,
|
|
||||||
ToolGeneratorTool
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. LLM 引擎
|
|
||||||
llm = LLMEngine(model_name="gpt-4o")
|
|
||||||
|
|
||||||
# 3. 记忆模块
|
|
||||||
memory = MemoryStore(max_history=20)
|
|
||||||
|
|
||||||
# 4. 组装客户端
|
|
||||||
client = AgentClient(llm=llm, mcp_server=mcp_server, memory=memory, prompt=agent_prompt)
|
|
||||||
|
|
||||||
logger.info(f"✅ Agent 组装完成,已注册工具: {mcp_server.list_tools()}")
|
logger.info(f"✅ Agent 组装完成,已注册工具: {mcp_server.list_tools()}")
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
# ── 演示场景 ───────────────────────────────────────────────────
|
# ── 结果打印 ───────────────────────────────────────────────────
|
||||||
def run_demo(client: AgentClient) -> None:
|
def print_response(response: AgentResponse) -> None:
|
||||||
"""运行预设演示场景,展示各工具的完整调用链路"""
|
"""格式化打印 AgentResponse"""
|
||||||
demo_cases = [
|
print(f"\n{'═' * 62}")
|
||||||
("🔢 数学计算", "计算 (100 + 200) × 3 等于多少"),
|
print(f"👤 用户: {response.user_input}")
|
||||||
("🌐 网络搜索", "搜索 Python 最新版本的新特性"),
|
print(f"{'─' * 62}")
|
||||||
("📄 文件读取", "读取文件 config.json 的内容"),
|
|
||||||
("🐍 代码执行", '执行代码 `print("Hello, Agent!")`'),
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.info("\n" + "═" * 60)
|
if response.chain_result:
|
||||||
logger.info("🎬 开始演示模式,共 4 个场景")
|
cr = response.chain_result
|
||||||
logger.info("═" * 60)
|
tag = "🔗 多步串行" if response.is_multi_step else "🔧 单步调用"
|
||||||
|
status = "✅ 全部成功" if cr.success else f"⚠️ 步骤 {cr.failed_step} 失败"
|
||||||
for title, question in demo_cases:
|
print(f"{tag} | {cr.completed_steps}/{cr.total_steps} 步 | {status}")
|
||||||
logger.info(f"\n{'─' * 55}")
|
print()
|
||||||
logger.info(f"📌 场景: {title}")
|
for r in cr.step_results:
|
||||||
logger.info(f"{'─' * 55}")
|
icon = "✅" if r.success else "❌"
|
||||||
|
preview = r.output.replace("\n", " ")[:90]
|
||||||
response = client.chat(question)
|
print(f" {icon} Step {r.step_id} [{r.tool_name}]")
|
||||||
|
if r.success:
|
||||||
print(f"\n{'─' * 55}")
|
print(f" └─ {preview}...")
|
||||||
print(f"👤 用户: {response.user_input}")
|
else:
|
||||||
if response.tool_used:
|
print(f" └─ 错误: {r.error}")
|
||||||
print(f"🔧 工具: {response.tool_used}")
|
|
||||||
print(f"📤 输出: {response.tool_output[:120]}...")
|
|
||||||
print(f"🤖 回复:\n{response.final_reply}")
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 打印记忆统计
|
print(f"🤖 Agent 回复:\n{response.final_reply}")
|
||||||
|
print(f"{'═' * 62}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ── API 健康检测 ───────────────────────────────────────────────
|
||||||
|
def run_health_check() -> None:
|
||||||
|
"""检测 OpenAI API 连通性"""
|
||||||
|
print(f"\n{'─' * 50}")
|
||||||
|
print(f" 🏥 OpenAI API 健康检测")
|
||||||
|
print(f"{'─' * 50}")
|
||||||
|
print(f" Provider : {settings.llm.provider}")
|
||||||
|
print(f" Model : {settings.llm.model_name}")
|
||||||
|
print(f" API Key : {'***' + settings.llm.api_key[-4:] if len(settings.llm.api_key) > 4 else '(未设置)'}")
|
||||||
|
print(f" Base URL : {settings.llm.api_base_url or 'https://api.openai.com/v1'}")
|
||||||
|
print(f"{'─' * 50}")
|
||||||
|
|
||||||
|
if not settings.llm.api_key:
|
||||||
|
print(" ❌ API Key 未设置")
|
||||||
|
print(" 💡 请设置环境变量: export LLM_API_KEY=sk-...")
|
||||||
|
print(f"{'─' * 50}\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(" ⏳ 正在检测连通性...")
|
||||||
|
llm = LLMEngine()
|
||||||
|
ok = llm.provider.health_check()
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
print(f" ✅ API 连通正常,模型 [{settings.llm.model_name}] 可用")
|
||||||
|
else:
|
||||||
|
print(f" ❌ API 连接失败,请检查网络或 API Key")
|
||||||
|
print(f" 💡 可尝试设置代理: export LLM_API_BASE_URL=https://your-proxy/v1")
|
||||||
|
print(f"{'─' * 50}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 演示场景 ───────────────────────────────────────────────────
|
||||||
|
def run_demo(client: AgentClient) -> None:
|
||||||
|
"""运行预设演示场景"""
|
||||||
|
demo_cases = [
|
||||||
|
("🔢 单步: 数学计算",
|
||||||
|
"计算 (100 + 200) × 3 等于多少?"),
|
||||||
|
|
||||||
|
("🌐 单步: 网络搜索",
|
||||||
|
"搜索 Python 3.12 的主要新特性"),
|
||||||
|
|
||||||
|
("🔗 两步: 搜索 + 计算",
|
||||||
|
"搜索 Python 最新版本号,然后计算 3.12 × 100 的结果"),
|
||||||
|
|
||||||
|
("🔗 两步: 读取文件 + 执行代码",
|
||||||
|
"读取 script.py 文件然后执行里面的代码"),
|
||||||
|
|
||||||
|
("💬 无工具: 直接问答",
|
||||||
|
"你好,请介绍一下你自己"),
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info("\n" + "═" * 62)
|
||||||
|
logger.info(f"🎬 演示模式 | 模型: {settings.llm.model_name} | "
|
||||||
|
f"Provider: {settings.llm.provider}")
|
||||||
|
logger.info("═" * 62)
|
||||||
|
|
||||||
|
for title, question in demo_cases:
|
||||||
|
logger.info(f"\n📌 场景: {title}")
|
||||||
|
response = client.chat(question)
|
||||||
|
print_response(response)
|
||||||
|
|
||||||
stats = client.get_memory_stats()
|
stats = client.get_memory_stats()
|
||||||
logger.info(f"\n📊 Memory 统计: {stats}")
|
print(f"📊 Memory 统计: {stats}\n")
|
||||||
|
|
||||||
|
|
||||||
# ── 交互式对话循环 ─────────────────────────────────────────────
|
# ── 交互模式 ───────────────────────────────────────────────────
|
||||||
def run_interactive(client: AgentClient) -> None:
|
def run_interactive(client: AgentClient) -> None:
|
||||||
"""启动交互式命令行对话"""
|
"""启动交互式命令行对话"""
|
||||||
print("\n" + "═" * 60)
|
print("\n" + "═" * 62)
|
||||||
print(" 🤖 Agent Demo — 交互模式")
|
print(f" 🤖 Agent | {settings.llm.model_name} | {settings.llm.provider}")
|
||||||
print(" 输入 'quit' → 退出程序")
|
print(f" Function Calling: {'✅ 开启' if settings.llm.function_calling else '❌ 关闭(规则引擎)'}")
|
||||||
print(" 输入 'clear' → 清空会话历史")
|
print(f" Fallback Rules : {'✅ 开启' if settings.agent.fallback_to_rules else '❌ 关闭'}")
|
||||||
print(" 输入 'stats' → 查看 Memory 统计")
|
print("─" * 62)
|
||||||
print(" 输入 'tools' → 查看已注册工具列表")
|
print(" 💡 示例:")
|
||||||
print("═" * 60 + "\n")
|
print(" 计算 (100+200) × 3")
|
||||||
|
print(" 搜索 Python 新特性,然后计算 3.12 × 100")
|
||||||
|
print(" 读取 config.json 文件然后执行代码")
|
||||||
|
print("─" * 62)
|
||||||
|
print(" 🛠 命令: config / health / tools / chains / stats / clear / quit")
|
||||||
|
print("═" * 62 + "\n")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
@ -113,48 +170,80 @@ def run_interactive(client: AgentClient) -> None:
|
||||||
if not user_input:
|
if not user_input:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ── 内置命令 ──────────────────────────────────────────
|
|
||||||
match user_input.lower():
|
match user_input.lower():
|
||||||
case "quit" | "exit":
|
case "quit" | "exit":
|
||||||
print("👋 再见!")
|
print("👋 再见!")
|
||||||
break
|
break
|
||||||
|
case "config":
|
||||||
|
print(settings.display())
|
||||||
|
case "health":
|
||||||
|
run_health_check()
|
||||||
case "clear":
|
case "clear":
|
||||||
client.clear_session()
|
client.clear_session()
|
||||||
print("✅ 会话已清空\n")
|
print("✅ 会话已清空\n")
|
||||||
continue
|
|
||||||
case "stats":
|
case "stats":
|
||||||
print(f"📊 Memory 统计: {client.get_memory_stats()}\n")
|
print(f"📊 {client.get_memory_stats()}\n")
|
||||||
continue
|
|
||||||
case "tools":
|
case "tools":
|
||||||
tools = client.mcp_server.list_tools()
|
schemas = client.mcp_server.get_tool_schemas()
|
||||||
print(f"🔧 已注册工具 ({len(tools)} 个): {', '.join(tools)}\n")
|
print(f"🔧 已注册工具 ({len(schemas)} 个):")
|
||||||
continue
|
for s in schemas:
|
||||||
|
print(f" • [{s.name}] {s.description}")
|
||||||
|
print()
|
||||||
|
case "chains":
|
||||||
|
chains = client.memory.get_chain_history()
|
||||||
|
if not chains:
|
||||||
|
print("🔗 暂无调用链历史\n")
|
||||||
|
else:
|
||||||
|
print(f"🔗 调用链历史 ({len(chains)} 条):")
|
||||||
|
for i, c in enumerate(chains, 1):
|
||||||
|
steps = " → ".join(s["tool_name"] for s in c["steps"])
|
||||||
|
ok_cnt = sum(1 for s in c["steps"] if s["success"])
|
||||||
|
total = len(c["steps"])
|
||||||
|
print(f" {i}. [{c['timestamp'][11:19]}] {c['goal'][:38]}...")
|
||||||
|
print(f" 链路: {steps} ({ok_cnt}/{total} 步成功)")
|
||||||
|
print()
|
||||||
|
case _:
|
||||||
|
response = client.chat(user_input)
|
||||||
|
print_response(response)
|
||||||
|
|
||||||
# ── 执行 Agent 完整流程 ───────────────────────────────
|
|
||||||
response = client.chat(user_input)
|
|
||||||
|
|
||||||
print(f"\n{'─' * 55}")
|
# ── 配置打印 ───────────────────────────────────────────────────
|
||||||
if response.tool_used:
|
def run_show_config() -> None:
|
||||||
print(f" 🔧 调用工具: {response.tool_used}")
|
print(settings.display())
|
||||||
print(f"🤖 Agent:\n{response.final_reply}")
|
print("\n📁 配置文件查找路径(按优先级):")
|
||||||
print(f"{'─' * 55}\n")
|
print(" 1. 环境变量 AGENT_CONFIG_PATH")
|
||||||
|
print(" 2. ./config/config.yaml")
|
||||||
|
print(" 3. ./config.yaml")
|
||||||
|
print("\n🌍 支持的环境变量覆盖:")
|
||||||
|
env_vars = [
|
||||||
|
("LLM_API_KEY", "OpenAI API 密钥(sk-...)"),
|
||||||
|
("LLM_MODEL_NAME", "模型名称,如 gpt-4o / gpt-4-turbo"),
|
||||||
|
("LLM_API_BASE_URL", "自定义 API 地址(兼容代理)"),
|
||||||
|
("LLM_MODEL_PATH", "本地模型路径"),
|
||||||
|
("SEARCH_API_KEY", "搜索 API 密钥"),
|
||||||
|
("LOG_LEVEL", "日志级别 DEBUG/INFO/WARNING/ERROR"),
|
||||||
|
("AGENT_CONFIG_PATH","配置文件路径"),
|
||||||
|
]
|
||||||
|
for var, desc in env_vars:
|
||||||
|
print(f" {var:<22} → {desc}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
# ── 主函数 ─────────────────────────────────────────────────────
|
# ── 主函数 ─────────────────────────────────────────────────────
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""
|
mode = sys.argv[1] if len(sys.argv) > 1 else "interactive"
|
||||||
主函数入口,支持两种运行模式:
|
|
||||||
|
|
||||||
python main.py → 交互模式(默认)
|
if mode == "config":
|
||||||
python main.py demo → 演示模式(自动执行预设场景)
|
run_show_config()
|
||||||
"""
|
return
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-d", "--daemon", help="服务模式", action="store_true")
|
|
||||||
parser.add_argument("-p", "--prompt", default="你是一个通用智能体,非常擅长将用户指令分解成可以执行的任务进行执行。", help="智能体提示此词, 例如:你是一个XXXXX,非常擅长……")
|
|
||||||
args = parser.parse_args(sys.argv[1:])
|
|
||||||
client = build_agent(args.prompt)
|
|
||||||
|
|
||||||
if args.daemon:
|
if mode == "health":
|
||||||
|
run_health_check()
|
||||||
|
return
|
||||||
|
|
||||||
|
client = build_agent()
|
||||||
|
|
||||||
|
if mode == "demo":
|
||||||
run_demo(client)
|
run_demo(client)
|
||||||
else:
|
else:
|
||||||
run_interactive(client)
|
run_interactive(client)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
"""MCP 协议:JSON-RPC 消息定义"""
|
|
||||||
"""
|
"""
|
||||||
mcp/mcp_protocol.py
|
mcp/mcp_protocol.py
|
||||||
MCP (Model Context Protocol) 协议数据结构定义
|
MCP 协议数据结构定义
|
||||||
基于 JSON-RPC 2.0 规范封装请求/响应消息体
|
新增: ToolStep / ChainPlan / StepResult / ChainResult 支持多工具串行调用
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -12,32 +11,32 @@ from typing import Any
|
||||||
|
|
||||||
# ── MCP 方法常量 ───────────────────────────────────────────────
|
# ── MCP 方法常量 ───────────────────────────────────────────────
|
||||||
class MCPMethod:
|
class MCPMethod:
|
||||||
TOOLS_LIST = "tools/list" # 列出所有可用工具
|
TOOLS_LIST = "tools/list"
|
||||||
TOOLS_CALL = "tools/call" # 调用指定工具
|
TOOLS_CALL = "tools/call"
|
||||||
RESOURCES_READ = "resources/read" # 读取资源
|
RESOURCES_READ = "resources/read"
|
||||||
|
|
||||||
|
|
||||||
# ── 请求消息 ───────────────────────────────────────────────────
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# JSON-RPC 基础消息
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MCPRequest:
|
class MCPRequest:
|
||||||
"""
|
"""
|
||||||
MCP 工具调用请求(JSON-RPC 2.0 格式)
|
MCP 工具调用请求(JSON-RPC 2.0)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "abc-123",
|
"id": "abc-123",
|
||||||
"method": "tools/call",
|
"method": "tools/call",
|
||||||
"params": {
|
"params": {"name": "calculator", "arguments": {"expression": "1+1"}}
|
||||||
"name": "calculator",
|
|
||||||
"arguments": {"expression": "1+1"}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
method: str
|
method: str
|
||||||
params: dict[str, Any] = field(default_factory=dict)
|
params: dict[str, Any] = field(default_factory=dict)
|
||||||
jsonrpc: str = "2.0"
|
jsonrpc: str = "2.0"
|
||||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
|
|
@ -48,22 +47,15 @@ class MCPRequest:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── 响应消息 ───────────────────────────────────────────────────
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MCPResponse:
|
class MCPResponse:
|
||||||
"""
|
"""
|
||||||
MCP 工具调用响应(JSON-RPC 2.0 格式)
|
MCP 工具调用响应(JSON-RPC 2.0)
|
||||||
|
|
||||||
成功示例:
|
|
||||||
{"jsonrpc": "2.0", "id": "abc-123", "result": {"content": [...]}}
|
|
||||||
|
|
||||||
失败示例:
|
|
||||||
{"jsonrpc": "2.0", "id": "abc-123", "error": {"code": -32601, "message": "..."}}
|
|
||||||
"""
|
"""
|
||||||
id: str
|
id: str
|
||||||
result: dict[str, Any] | None = None
|
result: dict[str, Any] | None = None
|
||||||
error: dict[str, Any] | None = None
|
error: dict[str, Any] | None = None
|
||||||
jsonrpc: str = "2.0"
|
jsonrpc: str = "2.0"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
|
|
@ -75,7 +67,9 @@ class MCPResponse:
|
||||||
if not self.success or not self.result:
|
if not self.success or not self.result:
|
||||||
return self.error.get("message", "Unknown error") if self.error else ""
|
return self.error.get("message", "Unknown error") if self.error else ""
|
||||||
items = self.result.get("content", [])
|
items = self.result.get("content", [])
|
||||||
return "\n".join(item.get("text", "") for item in items if item.get("type") == "text")
|
return "\n".join(
|
||||||
|
item.get("text", "") for item in items if item.get("type") == "text"
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
base = {"jsonrpc": self.jsonrpc, "id": self.id}
|
base = {"jsonrpc": self.jsonrpc, "id": self.id}
|
||||||
|
|
@ -86,15 +80,12 @@ class MCPResponse:
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
# ── 工具描述 ───────────────────────────────────────────────────
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ToolSchema:
|
class ToolSchema:
|
||||||
"""
|
"""工具元数据描述,供 LLM 识别和选择工具"""
|
||||||
工具的元数据描述,用于 LLM 识别和选择工具
|
|
||||||
"""
|
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
parameters: dict[str, Any] # JSON Schema 格式的参数定义
|
parameters: dict[str, Any]
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
|
|
@ -104,4 +95,147 @@ class ToolSchema:
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": self.parameters,
|
"properties": self.parameters,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
# 串行调用链数据结构(新增)
|
||||||
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolStep:
|
||||||
|
"""
|
||||||
|
调用链中的单个执行步骤
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
step_id: 步骤编号(从 1 开始)
|
||||||
|
tool_name: 要调用的工具名称
|
||||||
|
arguments: 工具参数(支持 {{STEP_N_OUTPUT}} 占位符引用前步结果)
|
||||||
|
description: 该步骤的自然语言说明
|
||||||
|
depends_on: 依赖的前置步骤编号列表(用于上下文注入)
|
||||||
|
|
||||||
|
占位符示例:
|
||||||
|
arguments = {"query": "{{STEP_1_OUTPUT}}"}
|
||||||
|
→ 执行时自动替换为第 1 步的输出内容
|
||||||
|
"""
|
||||||
|
step_id: int
|
||||||
|
tool_name: str
|
||||||
|
arguments: dict[str, Any]
|
||||||
|
description: str = ""
|
||||||
|
depends_on: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
def to_mcp_request(self) -> MCPRequest:
|
||||||
|
"""将步骤转换为 MCP 请求"""
|
||||||
|
return MCPRequest(
|
||||||
|
method=MCPMethod.TOOLS_CALL,
|
||||||
|
params={"name": self.tool_name, "arguments": self.arguments},
|
||||||
|
)
|
||||||
|
|
||||||
|
def inject_context(self, context: dict[str, str]) -> "ToolStep":
|
||||||
|
"""
|
||||||
|
将前步输出注入当前步骤的参数占位符
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: {"STEP_1_OUTPUT": "...", "STEP_2_OUTPUT": "..."}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
注入后的新 ToolStep(不修改原对象)
|
||||||
|
"""
|
||||||
|
resolved_args = {}
|
||||||
|
for key, value in self.arguments.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
for placeholder, replacement in context.items():
|
||||||
|
# 截取前 500 字符避免参数过长
|
||||||
|
value = value.replace(f"{{{{{placeholder}}}}}", replacement[:500])
|
||||||
|
resolved_args[key] = value
|
||||||
|
return ToolStep(
|
||||||
|
step_id=self.step_id,
|
||||||
|
tool_name=self.tool_name,
|
||||||
|
arguments=resolved_args,
|
||||||
|
description=self.description,
|
||||||
|
depends_on=self.depends_on,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChainPlan:
|
||||||
|
"""
|
||||||
|
完整的工具调用链执行计划
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
steps: 有序的步骤列表(按执行顺序排列)
|
||||||
|
goal: 整体目标描述
|
||||||
|
is_single: 是否为单步调用(优化路径)
|
||||||
|
"""
|
||||||
|
steps: list[ToolStep]
|
||||||
|
goal: str = ""
|
||||||
|
is_single: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_count(self) -> int:
|
||||||
|
return len(self.steps)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
steps_desc = " → ".join(
|
||||||
|
f"[{s.step_id}]{s.tool_name}" for s in self.steps
|
||||||
|
)
|
||||||
|
return f"ChainPlan(goal={self.goal!r}, chain={steps_desc})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StepResult:
|
||||||
|
"""
|
||||||
|
单个步骤的执行结果
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
step_id: 对应的步骤编号
|
||||||
|
tool_name: 执行的工具名称
|
||||||
|
success: 是否执行成功
|
||||||
|
output: 工具输出内容
|
||||||
|
error: 失败时的错误信息
|
||||||
|
"""
|
||||||
|
step_id: int
|
||||||
|
tool_name: str
|
||||||
|
success: bool
|
||||||
|
output: str
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context_key(self) -> str:
|
||||||
|
"""生成占位符 key,供后续步骤引用"""
|
||||||
|
return f"STEP_{self.step_id}_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChainResult:
|
||||||
|
"""
|
||||||
|
完整调用链的汇总结果
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
goal: 原始目标
|
||||||
|
step_results: 每步的执行结果列表
|
||||||
|
final_reply: LLM 整合后的最终回复
|
||||||
|
success: 整体是否成功
|
||||||
|
failed_step: 首个失败步骤编号(成功时为 None)
|
||||||
|
"""
|
||||||
|
goal: str
|
||||||
|
step_results: list[StepResult]
|
||||||
|
final_reply: str = ""
|
||||||
|
success: bool = True
|
||||||
|
failed_step: int | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed_steps(self) -> int:
|
||||||
|
return sum(1 for r in self.step_results if r.success)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_steps(self) -> int:
|
||||||
|
return len(self.step_results)
|
||||||
|
|
||||||
|
def get_summary(self) -> str:
|
||||||
|
"""生成执行摘要字符串"""
|
||||||
|
lines = [f"📋 执行计划: {self.goal}", f"📊 完成步骤: {self.completed_steps}/{self.total_steps}"]
|
||||||
|
for r in self.step_results:
|
||||||
|
icon = "✅" if r.success else "❌"
|
||||||
|
lines.append(f" {icon} Step {r.step_id} [{r.tool_name}]: {r.output[:60]}...")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
"""MCP 服务器:工具注册 & 调度"""
|
|
||||||
"""
|
"""
|
||||||
mcp/mcp_server.py
|
mcp/mcp_server.py
|
||||||
MCP Server:工具注册中心与调度引擎
|
MCP Server:从配置读取 server_name、transport、enabled_tools
|
||||||
负责管理所有工具的生命周期,处理 JSON-RPC 格式的工具调用请求
|
支持按配置动态过滤注册工具
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from config.settings import MCPConfig, settings
|
||||||
from mcp.mcp_protocol import MCPMethod, MCPRequest, MCPResponse, ToolSchema
|
from mcp.mcp_protocol import MCPMethod, MCPRequest, MCPResponse, ToolSchema
|
||||||
from tools.base_tool import BaseTool, ToolResult
|
from tools.base_tool import BaseTool, ToolResult
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
@ -15,40 +14,53 @@ from utils.logger import get_logger
|
||||||
|
|
||||||
class MCPServer:
|
class MCPServer:
|
||||||
"""
|
"""
|
||||||
MCP 服务器核心类
|
MCP 服务器核心类(配置驱动)
|
||||||
|
|
||||||
职责:
|
配置项:
|
||||||
1. 工具注册(register_tool)
|
- server_name: 服务器名称
|
||||||
2. 工具列表查询(tools/list)
|
- transport: 通信方式 (stdio / http / websocket)
|
||||||
3. 工具调用分发(tools/call)
|
- enabled_tools: 白名单,仅注册列表中的工具
|
||||||
4. JSON-RPC 协议封装/解析
|
|
||||||
|
|
||||||
使用示例:
|
使用示例:
|
||||||
server = MCPServer()
|
server = MCPServer() # 从 settings 读取配置
|
||||||
|
server = MCPServer(cfg=custom_cfg) # 使用自定义配置
|
||||||
server.register_tool(CalculatorTool)
|
server.register_tool(CalculatorTool)
|
||||||
response = server.handle_request(request)
|
response = server.handle_request(request)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server_name: str = "AgentMCPServer"):
|
def __init__(self, cfg: MCPConfig | None = None):
|
||||||
self.server_name = server_name
|
"""
|
||||||
|
Args:
|
||||||
|
cfg: MCPConfig 实例,None 时从全局 settings 读取
|
||||||
|
"""
|
||||||
|
self.cfg = cfg or settings.mcp
|
||||||
self.logger = get_logger("MCP")
|
self.logger = get_logger("MCP")
|
||||||
self._registry: dict[str, BaseTool] = {} # 工具名 → 工具实例
|
self._registry: dict[str, BaseTool] = {}
|
||||||
|
|
||||||
self.logger.info(f"🚀 MCP Server [{server_name}] 启动")
|
self.logger.info(f"🚀 MCP Server [{self.cfg.server_name}] 启动")
|
||||||
|
self.logger.info(f" transport = {self.cfg.transport}")
|
||||||
|
self.logger.info(f" enabled_tools = {self.cfg.enabled_tools}")
|
||||||
|
|
||||||
# ── 工具注册 ────────────────────────────────────────────────
|
# ── 工具注册 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def register_tool(self, tool_class: Type[BaseTool]) -> None:
|
def register_tool(self, tool_class: Type[BaseTool]) -> None:
|
||||||
"""
|
"""
|
||||||
注册一个工具类到服务器
|
注册工具(受 enabled_tools 白名单过滤)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tool_class: 继承自 BaseTool 的工具类(传入类本身,不是实例)
|
tool_class: 继承自 BaseTool 的工具类
|
||||||
"""
|
"""
|
||||||
instance = tool_class()
|
instance = tool_class()
|
||||||
if not instance.name:
|
if not instance.name:
|
||||||
raise ValueError(f"工具类 {tool_class.__name__} 未设置 name 属性")
|
raise ValueError(f"工具类 {tool_class.__name__} 未设置 name 属性")
|
||||||
|
|
||||||
|
# 白名单过滤
|
||||||
|
if instance.name not in self.cfg.enabled_tools:
|
||||||
|
self.logger.warning(
|
||||||
|
f"⏭ 工具 [{instance.name}] 不在 enabled_tools 白名单中,跳过注册"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
self._registry[instance.name] = instance
|
self._registry[instance.name] = instance
|
||||||
self.logger.info(f"📌 注册工具: [{instance.name}] — {instance.description}")
|
self.logger.info(f"📌 注册工具: [{instance.name}] — {instance.description}")
|
||||||
|
|
||||||
|
|
@ -57,87 +69,61 @@ class MCPServer:
|
||||||
for cls in tool_classes:
|
for cls in tool_classes:
|
||||||
self.register_tool(cls)
|
self.register_tool(cls)
|
||||||
|
|
||||||
# ── 请求处理入口 ────────────────────────────────────────────
|
# ── 请求处理 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def handle_request(self, request: MCPRequest) -> MCPResponse:
|
def handle_request(self, request: MCPRequest) -> MCPResponse:
|
||||||
"""
|
"""处理 MCP 请求的统一入口"""
|
||||||
处理 MCP 请求的统一入口,根据 method 分发到对应处理器
|
self.logger.info(
|
||||||
|
f"📨 收到请求 id={request.id} method={request.method} "
|
||||||
Args:
|
f"transport={self.cfg.transport}"
|
||||||
request: MCPRequest 实例
|
)
|
||||||
|
|
||||||
Returns:
|
|
||||||
MCPResponse 实例
|
|
||||||
"""
|
|
||||||
self.logger.info(f"📨 收到请求 id={request.id} method={request.method}")
|
|
||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
MCPMethod.TOOLS_LIST: self._handle_tools_list,
|
MCPMethod.TOOLS_LIST: self._handle_tools_list,
|
||||||
MCPMethod.TOOLS_CALL: self._handle_tools_call,
|
MCPMethod.TOOLS_CALL: self._handle_tools_call,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = handlers.get(request.method)
|
handler = handlers.get(request.method)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
return self._error_response(request.id, -32601, f"未知方法: {request.method}")
|
return self._error_response(request.id, -32601, f"未知方法: {request.method}")
|
||||||
|
|
||||||
return handler(request)
|
return handler(request)
|
||||||
|
|
||||||
# ── 私有处理器 ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _handle_tools_list(self, request: MCPRequest) -> MCPResponse:
|
def _handle_tools_list(self, request: MCPRequest) -> MCPResponse:
|
||||||
"""处理 tools/list 请求,返回所有已注册工具的 Schema"""
|
|
||||||
schemas = [tool.get_schema().to_dict() for tool in self._registry.values()]
|
schemas = [tool.get_schema().to_dict() for tool in self._registry.values()]
|
||||||
self.logger.info(f"📋 返回工具列表,共 {len(schemas)} 个工具")
|
self.logger.info(f"📋 返回工具列表,共 {len(schemas)} 个")
|
||||||
return MCPResponse(
|
return MCPResponse(id=request.id, result={"tools": schemas})
|
||||||
id=request.id,
|
|
||||||
result={"tools": schemas},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_tools_call(self, request: MCPRequest) -> MCPResponse:
|
def _handle_tools_call(self, request: MCPRequest) -> MCPResponse:
|
||||||
"""处理 tools/call 请求,调用指定工具并返回结果"""
|
|
||||||
tool_name = request.params.get("name")
|
tool_name = request.params.get("name")
|
||||||
arguments = request.params.get("arguments", {})
|
arguments = request.params.get("arguments", {})
|
||||||
|
tool = self._registry.get(tool_name)
|
||||||
# 检查工具是否存在
|
|
||||||
tool = self._registry.get(tool_name)
|
|
||||||
if tool is None:
|
if tool is None:
|
||||||
available = list(self._registry.keys())
|
|
||||||
return self._error_response(
|
return self._error_response(
|
||||||
request.id, -32602,
|
request.id, -32602,
|
||||||
f"工具 [{tool_name}] 不存在,可用工具: {available}"
|
f"工具 [{tool_name}] 不存在,可用: {list(self._registry.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 执行工具
|
|
||||||
result: ToolResult = tool.safe_execute(**arguments)
|
result: ToolResult = tool.safe_execute(**arguments)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
return MCPResponse(
|
return MCPResponse(
|
||||||
id=request.id,
|
id=request.id,
|
||||||
result={
|
result={"content": [{"type": "text", "text": result.output}],
|
||||||
"content": [{"type": "text", "text": result.output}],
|
"metadata": result.metadata},
|
||||||
"metadata": result.metadata,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
else:
|
return self._error_response(request.id, -32000, result.output)
|
||||||
return self._error_response(request.id, -32000, result.output)
|
|
||||||
|
|
||||||
# ── 工具方法 ────────────────────────────────────────────────
|
# ── 工具方法 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_tool_schemas(self) -> list[ToolSchema]:
|
def get_tool_schemas(self) -> list[ToolSchema]:
|
||||||
"""获取所有工具的 Schema 列表(供 LLM 引擎使用)"""
|
|
||||||
return [tool.get_schema() for tool in self._registry.values()]
|
return [tool.get_schema() for tool in self._registry.values()]
|
||||||
|
|
||||||
def list_tools(self) -> list[str]:
|
def list_tools(self) -> list[str]:
|
||||||
"""返回所有已注册工具的名称列表"""
|
|
||||||
return list(self._registry.keys())
|
return list(self._registry.keys())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _error_response(req_id: str, code: int, message: str) -> MCPResponse:
|
def _error_response(req_id: str, code: int, message: str) -> MCPResponse:
|
||||||
"""构造标准 JSON-RPC 错误响应"""
|
return MCPResponse(id=req_id, error={"code": code, "message": message})
|
||||||
return MCPResponse(
|
|
||||||
id=req_id,
|
|
||||||
error={"code": code, "message": message},
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"MCPServer(name={self.server_name!r}, tools={self.list_tools()})"
|
return (
|
||||||
|
f"MCPServer(name={self.cfg.server_name!r}, "
|
||||||
|
f"transport={self.cfg.transport!r}, "
|
||||||
|
f"tools={self.list_tools()})"
|
||||||
|
)
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
"""记忆模块:对话历史管理"""
|
|
||||||
"""
|
"""
|
||||||
memory/memory_store.py
|
memory/memory_store.py
|
||||||
Agent 记忆模块:管理对话历史(短期记忆)与关键信息摘要(长期记忆)
|
Agent 记忆模块:管理对话历史(短期)与关键事实(长期)
|
||||||
|
新增: add_chain_result() 记录完整多步骤调用链
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from mcp.mcp_protocol import ChainResult
|
||||||
|
|
||||||
|
|
||||||
# ── 消息数据结构 ───────────────────────────────────────────────
|
# ── 消息数据结构 ───────────────────────────────────────────────
|
||||||
@dataclass
|
@dataclass
|
||||||
class Message:
|
class Message:
|
||||||
"""单条对话消息"""
|
"""单条对话消息"""
|
||||||
role: Literal["user", "assistant", "tool"]
|
role: Literal["user", "assistant", "tool", "chain"]
|
||||||
content: str
|
content: str
|
||||||
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))
|
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))
|
||||||
metadata: dict = field(default_factory=dict)
|
metadata: dict = field(default_factory=dict)
|
||||||
|
|
@ -34,74 +37,102 @@ class MemoryStore:
|
||||||
"""
|
"""
|
||||||
对话记忆存储
|
对话记忆存储
|
||||||
|
|
||||||
短期记忆: 使用 deque 保存最近 N 轮对话,自动滚动淘汰旧消息
|
短期记忆: deque 保存最近 N 轮对话,自动滚动淘汰
|
||||||
长期记忆: 保存关键事实摘要(生产环境可替换为向量数据库)
|
长期记忆: 关键事实列表(生产环境可替换为向量数据库)
|
||||||
|
链路记录: 完整的多步骤调用链历史
|
||||||
|
|
||||||
使用示例:
|
使用示例:
|
||||||
memory = MemoryStore(max_history=10)
|
memory = MemoryStore(max_history=20)
|
||||||
memory.add_user_message("你好")
|
memory.add_user_message("你好")
|
||||||
memory.add_assistant_message("你好!有什么可以帮你?")
|
memory.add_chain_result(chain_result)
|
||||||
history = memory.get_history()
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, max_history: int = 20):
|
def __init__(self, max_history: int = 20):
|
||||||
"""
|
self.logger = get_logger("MEMORY")
|
||||||
Args:
|
self.max_history = max_history
|
||||||
max_history: 短期记忆保留的最大消息条数
|
self._history: deque[Message] = deque(maxlen=max_history)
|
||||||
"""
|
self._facts: list[str] = []
|
||||||
self.logger = get_logger("MEMORY")
|
self._chains: list[dict] = [] # 调用链历史记录
|
||||||
self.max_history = max_history
|
|
||||||
self._history: deque[Message] = deque(maxlen=max_history)
|
|
||||||
self._facts: list[str] = [] # 长期记忆:关键事实
|
|
||||||
|
|
||||||
self.logger.info(f"💾 Memory 初始化,最大历史: {max_history} 条")
|
self.logger.info(f"💾 Memory 初始化,最大历史: {max_history} 条")
|
||||||
|
|
||||||
# ── 写入接口 ────────────────────────────────────────────────
|
# ── 写入接口 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def add_user_message(self, content: str) -> None:
|
def add_user_message(self, content: str) -> None:
|
||||||
"""记录用户消息"""
|
|
||||||
self._add(Message(role="user", content=content))
|
self._add(Message(role="user", content=content))
|
||||||
|
|
||||||
def add_assistant_message(self, content: str) -> None:
|
def add_assistant_message(self, content: str) -> None:
|
||||||
"""记录 Agent 回复"""
|
|
||||||
self._add(Message(role="assistant", content=content))
|
self._add(Message(role="assistant", content=content))
|
||||||
|
|
||||||
def add_tool_result(self, tool_name: str, result: str) -> None:
|
def add_tool_result(self, tool_name: str, result: str) -> None:
|
||||||
"""记录工具调用结果"""
|
|
||||||
self._add(Message(
|
self._add(Message(
|
||||||
role="tool",
|
role="tool",
|
||||||
content=result,
|
content=result,
|
||||||
metadata={"tool": tool_name},
|
metadata={"tool": tool_name},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def add_chain_result(self, chain_result: "ChainResult") -> None:
|
||||||
|
"""
|
||||||
|
记录完整的多步骤调用链结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chain_result: ChainResult 实例
|
||||||
|
"""
|
||||||
|
# 写入对话历史(assistant 角色)
|
||||||
|
self._add(Message(
|
||||||
|
role="chain",
|
||||||
|
content=chain_result.final_reply,
|
||||||
|
metadata={
|
||||||
|
"goal": chain_result.goal,
|
||||||
|
"total_steps": chain_result.total_steps,
|
||||||
|
"completed_steps": chain_result.completed_steps,
|
||||||
|
"success": chain_result.success,
|
||||||
|
"tools_used": [r.tool_name for r in chain_result.step_results],
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
# 写入链路追踪记录
|
||||||
|
chain_record = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"goal": chain_result.goal,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": r.step_id,
|
||||||
|
"tool_name": r.tool_name,
|
||||||
|
"success": r.success,
|
||||||
|
"output": r.output[:200],
|
||||||
|
"error": r.error,
|
||||||
|
}
|
||||||
|
for r in chain_result.step_results
|
||||||
|
],
|
||||||
|
"success": chain_result.success,
|
||||||
|
}
|
||||||
|
self._chains.append(chain_record)
|
||||||
|
self.logger.info(
|
||||||
|
f"🔗 调用链已记录: {chain_result.completed_steps}/{chain_result.total_steps} 步成功"
|
||||||
|
)
|
||||||
|
|
||||||
def add_fact(self, fact: str) -> None:
|
def add_fact(self, fact: str) -> None:
|
||||||
"""向长期记忆中添加关键事实"""
|
|
||||||
self._facts.append(fact)
|
self._facts.append(fact)
|
||||||
self.logger.debug(f"📌 长期记忆新增: {fact}")
|
self.logger.debug(f"📌 长期记忆新增: {fact}")
|
||||||
|
|
||||||
# ── 读取接口 ────────────────────────────────────────────────
|
# ── 读取接口 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_history(self, last_n: int | None = None) -> list[dict]:
|
def get_history(self, last_n: int | None = None) -> list[dict]:
|
||||||
"""
|
|
||||||
获取对话历史(LLM 上下文格式)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
last_n: 仅返回最近 N 条,None 表示全部
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
消息字典列表,格式: [{"role": ..., "content": ...}, ...]
|
|
||||||
"""
|
|
||||||
messages = list(self._history)
|
messages = list(self._history)
|
||||||
if last_n:
|
if last_n:
|
||||||
messages = messages[-last_n:]
|
messages = messages[-last_n:]
|
||||||
return [m.to_dict() for m in messages]
|
return [m.to_dict() for m in messages]
|
||||||
|
|
||||||
def get_facts(self) -> list[str]:
|
def get_facts(self) -> list[str]:
|
||||||
"""获取所有长期记忆事实"""
|
|
||||||
return list(self._facts)
|
return list(self._facts)
|
||||||
|
|
||||||
|
def get_chain_history(self) -> list[dict]:
|
||||||
|
"""获取所有调用链历史记录"""
|
||||||
|
return list(self._chains)
|
||||||
|
|
||||||
def get_context_summary(self) -> str:
|
def get_context_summary(self) -> str:
|
||||||
"""生成上下文摘要字符串,供 LLM Prompt 使用"""
|
"""生成上下文摘要,供 LLM Prompt 使用"""
|
||||||
history = self.get_history(last_n=6)
|
history = self.get_history(last_n=6)
|
||||||
lines = [f"[{m['role'].upper()}] {m['content'][:80]}" for m in history]
|
lines = [f"[{m['role'].upper()}] {m['content'][:80]}" for m in history]
|
||||||
return "\n".join(lines) if lines else "(暂无对话历史)"
|
return "\n".join(lines) if lines else "(暂无对话历史)"
|
||||||
|
|
@ -109,20 +140,17 @@ class MemoryStore:
|
||||||
# ── 管理接口 ────────────────────────────────────────────────
|
# ── 管理接口 ────────────────────────────────────────────────
|
||||||
|
|
||||||
def clear_history(self) -> None:
|
def clear_history(self) -> None:
|
||||||
"""清空短期对话历史"""
|
|
||||||
self._history.clear()
|
self._history.clear()
|
||||||
self.logger.info("🗑 对话历史已清空")
|
self.logger.info("🗑 对话历史已清空")
|
||||||
|
|
||||||
def stats(self) -> dict:
|
def stats(self) -> dict:
|
||||||
"""返回记忆统计信息"""
|
|
||||||
return {
|
return {
|
||||||
"history_count": len(self._history),
|
"history_count": len(self._history),
|
||||||
"facts_count": len(self._facts),
|
"facts_count": len(self._facts),
|
||||||
|
"chain_count": len(self._chains),
|
||||||
"max_history": self.max_history,
|
"max_history": self.max_history,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 私有方法 ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _add(self, message: Message) -> None:
|
def _add(self, message: Message) -> None:
|
||||||
self._history.append(message)
|
self._history.append(message)
|
||||||
self.logger.debug(f"💬 [{message.role.upper()}] {message.content[:60]}...")
|
self.logger.debug(f"💬 [{message.role.upper()}] {message.content[:60]}...")
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
openai
|
openai
|
||||||
|
pyyaml
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
"""计算器工具"""
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# tools/calculator.py — 数学计算工具
|
# tools/calculator.py
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
"""
|
"""安全的数学表达式计算工具(AST 解析,防注入)"""
|
||||||
tools/calculator.py
|
|
||||||
安全的数学表达式计算工具(使用 ast 模块避免 eval 注入风险)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
from config.settings import settings
|
||||||
from tools.base_tool import BaseTool, ToolResult
|
from tools.base_tool import BaseTool, ToolResult
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,27 +14,27 @@ class CalculatorTool(BaseTool):
|
||||||
name = "calculator"
|
name = "calculator"
|
||||||
description = "计算数学表达式,支持加减乘除、幂运算、括号等"
|
description = "计算数学表达式,支持加减乘除、幂运算、括号等"
|
||||||
parameters = {
|
parameters = {
|
||||||
"expression": {
|
"expression": {"type": "string", "description": "数学表达式,例如 '(1+2)*3'"},
|
||||||
"type": "string",
|
|
||||||
"description": "数学表达式,例如 '(1+2)*3' 或 '2**10'",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 允许的运算符白名单(防止注入)
|
|
||||||
_OPERATORS = {
|
_OPERATORS = {
|
||||||
ast.Add: operator.add,
|
ast.Add: operator.add, ast.Sub: operator.sub,
|
||||||
ast.Sub: operator.sub,
|
ast.Mult: operator.mul, ast.Div: operator.truediv,
|
||||||
ast.Mult: operator.mul,
|
ast.Pow: operator.pow, ast.Mod: operator.mod,
|
||||||
ast.Div: operator.truediv,
|
|
||||||
ast.Pow: operator.pow,
|
|
||||||
ast.Mod: operator.mod,
|
|
||||||
ast.USub: operator.neg,
|
ast.USub: operator.neg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# 从配置读取精度
|
||||||
|
self._precision = settings.tools.calculator.precision
|
||||||
|
self.logger.debug(f"⚙️ Calculator 精度: {self._precision}")
|
||||||
|
|
||||||
def execute(self, expression: str, **_) -> ToolResult:
|
def execute(self, expression: str, **_) -> ToolResult:
|
||||||
try:
|
try:
|
||||||
tree = ast.parse(expression, mode="eval")
|
tree = ast.parse(expression, mode="eval")
|
||||||
result = self._eval_node(tree.body)
|
result = self._eval_node(tree.body)
|
||||||
|
result = round(result, self._precision)
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
success=True,
|
success=True,
|
||||||
output=f"{expression} = {result}",
|
output=f"{expression} = {result}",
|
||||||
|
|
@ -46,7 +44,6 @@ class CalculatorTool(BaseTool):
|
||||||
return ToolResult(success=False, output=f"计算错误: {exc}")
|
return ToolResult(success=False, output=f"计算错误: {exc}")
|
||||||
|
|
||||||
def _eval_node(self, node: ast.AST) -> float:
|
def _eval_node(self, node: ast.AST) -> float:
|
||||||
"""递归解析 AST 节点"""
|
|
||||||
match node:
|
match node:
|
||||||
case ast.Constant(value=v) if isinstance(v, (int, float)):
|
case ast.Constant(value=v) if isinstance(v, (int, float)):
|
||||||
return v
|
return v
|
||||||
|
|
@ -61,4 +58,4 @@ class CalculatorTool(BaseTool):
|
||||||
raise ValueError(f"不支持的一元运算符: {type(op).__name__}")
|
raise ValueError(f"不支持的一元运算符: {type(op).__name__}")
|
||||||
return fn(self._eval_node(operand))
|
return fn(self._eval_node(operand))
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"不支持的表达式节点: {type(node).__name__}")
|
raise ValueError(f"不支持的节点: {type(node).__name__}")
|
||||||
|
|
@ -1,34 +1,23 @@
|
||||||
"""代码执行工具"""
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# tools/code_executor.py — 代码执行工具
|
# tools/code_executor.py
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
"""
|
"""沙箱代码执行工具(从配置读取 timeout / sandbox)"""
|
||||||
tools/code_executor.py
|
|
||||||
沙箱代码执行工具:在受限环境中运行 Python 代码片段
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import contextlib
|
import contextlib
|
||||||
import time
|
import time
|
||||||
from tools.base_tool import BaseTool, ToolResult
|
from tools.base_tool import BaseTool, ToolResult
|
||||||
|
from config.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class CodeExecutorTool(BaseTool):
|
class CodeExecutorTool(BaseTool):
|
||||||
name = "code_executor"
|
name = "code_executor"
|
||||||
description = "在沙箱环境中执行 Python 代码片段,返回标准输出"
|
description = "在沙箱环境中执行 Python 代码片段,返回标准输出"
|
||||||
parameters = {
|
parameters = {
|
||||||
"code": {
|
"code": {"type": "string", "description": "要执行的 Python 代码"},
|
||||||
"type": "string",
|
"timeout": {"type": "integer", "description": "超时时间(秒)"},
|
||||||
"description": "要执行的 Python 代码",
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "超时时间(秒),默认 5",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 沙箱:仅允许安全的内置函数
|
|
||||||
_SAFE_BUILTINS = {
|
_SAFE_BUILTINS = {
|
||||||
"print": print, "range": range, "len": len,
|
"print": print, "range": range, "len": len,
|
||||||
"int": int, "float": float, "str": str, "list": list,
|
"int": int, "float": float, "str": str, "list": list,
|
||||||
|
|
@ -38,24 +27,35 @@ class CodeExecutorTool(BaseTool):
|
||||||
"sorted": sorted, "reversed": reversed,
|
"sorted": sorted, "reversed": reversed,
|
||||||
}
|
}
|
||||||
|
|
||||||
def execute(self, code: str, timeout: int = 5, **_) -> ToolResult:
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
cfg = settings.tools.code_executor
|
||||||
|
self._timeout = cfg.timeout
|
||||||
|
self._sandbox = cfg.sandbox
|
||||||
|
self.logger.debug(
|
||||||
|
f"⚙️ CodeExecutor timeout={self._timeout}s, sandbox={self._sandbox}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, code: str, timeout: int | None = None, **_) -> ToolResult:
|
||||||
|
timeout = timeout or self._timeout
|
||||||
stdout_buf = io.StringIO()
|
stdout_buf = io.StringIO()
|
||||||
start_time = time.perf_counter()
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
exec_globals = (
|
||||||
|
{"__builtins__": self._SAFE_BUILTINS}
|
||||||
|
if self._sandbox
|
||||||
|
else {"__builtins__": __builtins__}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 重定向 stdout,捕获 print 输出
|
|
||||||
with contextlib.redirect_stdout(stdout_buf):
|
with contextlib.redirect_stdout(stdout_buf):
|
||||||
exec( # noqa: S102
|
exec(compile(code, "<agent_sandbox>", "exec"), exec_globals) # noqa: S102
|
||||||
compile(code, "<agent_sandbox>", "exec"),
|
elapsed = (time.perf_counter() - start) * 1000
|
||||||
{"__builtins__": self._SAFE_BUILTINS},
|
|
||||||
)
|
|
||||||
elapsed = (time.perf_counter() - start_time) * 1000
|
|
||||||
output = stdout_buf.getvalue() or "(无输出)"
|
output = stdout_buf.getvalue() or "(无输出)"
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
success=True,
|
success=True,
|
||||||
output=f"执行成功 ({elapsed:.1f}ms):\n{output}",
|
output=f"执行成功 ({elapsed:.1f}ms) [sandbox={self._sandbox}]:\n{output}",
|
||||||
metadata={"elapsed_ms": elapsed},
|
metadata={"elapsed_ms": elapsed, "sandbox": self._sandbox},
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ToolResult(success=False, output=f"执行错误: {type(exc).__name__}: {exc}")
|
return ToolResult(success=False, output=f"执行错误: {type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
|
|
@ -1,65 +1,63 @@
|
||||||
"""文件读取工具"""
|
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# tools/file_reader.py — 文件读取工具
|
# tools/file_reader.py
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
"""
|
"""文件读取工具(从配置读取 allowed_root / max_file_size_kb)"""
|
||||||
tools/file_reader.py
|
|
||||||
本地文件读取工具,支持文本文件,限制读取路径防止越权
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools.base_tool import BaseTool, ToolResult
|
from tools.base_tool import BaseTool, ToolResult
|
||||||
|
from config.settings import settings
|
||||||
|
|
||||||
# 允许读取的根目录(沙箱限制)
|
|
||||||
_ALLOWED_ROOT = Path("./workspace")
|
|
||||||
|
|
||||||
|
|
||||||
class FileReaderTool(BaseTool):
|
class FileReaderTool(BaseTool):
|
||||||
name = "file_reader"
|
name = "file_reader"
|
||||||
description = "读取本地文件内容,仅限 workspace/ 目录下的文件"
|
description = "读取本地文件内容,仅限配置的 allowed_root 目录"
|
||||||
parameters = {
|
parameters = {
|
||||||
"path": {
|
"path": {"type": "string", "description": "文件路径(相对于 allowed_root)"},
|
||||||
"type": "string",
|
"encoding": {"type": "string", "description": "文件编码,默认 utf-8"},
|
||||||
"description": "文件路径,相对于 workspace/ 目录",
|
|
||||||
},
|
|
||||||
"encoding": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "文件编码,默认 utf-8",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def execute(self, path: str, encoding: str = "utf-8", **_) -> ToolResult:
|
def __init__(self):
|
||||||
_ALLOWED_ROOT.mkdir(exist_ok=True)
|
super().__init__()
|
||||||
|
cfg = settings.tools.file_reader
|
||||||
|
self._allowed_root = Path(cfg.allowed_root)
|
||||||
|
self._max_size_kb = cfg.max_file_size_kb
|
||||||
|
self.logger.debug(
|
||||||
|
f"⚙️ FileReader root={self._allowed_root}, "
|
||||||
|
f"max_size={self._max_size_kb}KB"
|
||||||
|
)
|
||||||
|
|
||||||
# 路径安全检查:防止目录穿越攻击
|
def execute(self, path: str, encoding: str = "utf-8", **_) -> ToolResult:
|
||||||
target = (_ALLOWED_ROOT / path).resolve()
|
self._allowed_root.mkdir(parents=True, exist_ok=True)
|
||||||
if not str(target).startswith(str(_ALLOWED_ROOT.resolve())):
|
|
||||||
return ToolResult(success=False, output=f"❌ 拒绝访问: 路径超出允许范围")
|
target = (self._allowed_root / path).resolve()
|
||||||
|
if not str(target).startswith(str(self._allowed_root.resolve())):
|
||||||
|
return ToolResult(success=False, output="❌ 拒绝访问: 路径超出允许范围")
|
||||||
|
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
# Demo 模式:自动创建示例文件
|
|
||||||
self._create_demo_file(target)
|
self._create_demo_file(target)
|
||||||
|
|
||||||
|
size_kb = target.stat().st_size / 1024
|
||||||
|
if size_kb > self._max_size_kb:
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
output=f"❌ 文件过大: {size_kb:.1f}KB > 限制 {self._max_size_kb}KB",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = target.read_text(encoding=encoding)
|
content = target.read_text(encoding=encoding)
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
success=True,
|
success=True,
|
||||||
output=f"文件 [{path}] 内容:\n{content}",
|
output=f"文件 [{path}] ({size_kb:.1f}KB):\n{content}",
|
||||||
metadata={"path": str(target), "size": target.stat().st_size},
|
metadata={"path": str(target), "size_kb": size_kb},
|
||||||
)
|
)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
return ToolResult(success=False, output=f"读取失败: {exc}")
|
return ToolResult(success=False, output=f"读取失败: {exc}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_demo_file(path: Path) -> None:
|
def _create_demo_file(path: Path) -> None:
|
||||||
"""自动创建演示用文件"""
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(
|
path.write_text(
|
||||||
'{\n "app": "AgentDemo",\n "version": "1.0.0",\n'
|
'{\n "app": "AgentDemo",\n "version": "1.0.0",\n'
|
||||||
' "llm": "claude-sonnet-4-6",\n "tools": ["calculator", "web_search"]\n}\n',
|
' "llm": "claude-sonnet-4-6",\n "tools": ["calculator","web_search"]\n}\n',
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
"""网络搜索工具"""
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# tools/web_search.py — 网络搜索工具(模拟)
|
# tools/web_search.py
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
"""
|
"""网络搜索工具(从配置读取 max_results / engine / api_key)"""
|
||||||
tools/web_search.py
|
|
||||||
网络搜索工具(Demo 中使用模拟数据,生产环境可替换为真实 API)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from tools.base_tool import BaseTool, ToolResult
|
from tools.base_tool import BaseTool, ToolResult
|
||||||
|
from config.settings import settings
|
||||||
|
|
||||||
|
|
||||||
# 模拟搜索结果数据库
|
|
||||||
_MOCK_RESULTS: dict[str, list[dict]] = {
|
_MOCK_RESULTS: dict[str, list[dict]] = {
|
||||||
"天气": [
|
"天气": [{"title": "今日天气预报", "snippet": "晴转多云,气温 15°C ~ 24°C,东南风 3 级"},
|
||||||
{"title": "今日天气预报", "snippet": "晴转多云,气温 15°C ~ 24°C,东南风 3 级"},
|
{"title": "未来 7 天天气", "snippet": "本周整体晴好,周末有小雨"}],
|
||||||
{"title": "未来 7 天天气", "snippet": "本周整体晴好,周末有小雨"},
|
"python":[{"title": "Python 官方文档", "snippet": "Python 3.12 新特性:改进的错误提示"},
|
||||||
],
|
{"title": "Python 教程", "snippet": "从零开始学 Python,包含 300+ 实战案例"}],
|
||||||
"python": [
|
|
||||||
{"title": "Python 官方文档", "snippet": "Python 3.12 新特性:改进的错误提示、更快的启动速度"},
|
|
||||||
{"title": "Python 教程", "snippet": "从零开始学 Python,包含 300+ 实战案例"},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
_DEFAULT_RESULTS = [
|
_DEFAULT_RESULTS = [
|
||||||
{"title": "搜索结果 1", "snippet": "找到相关内容,请查看详情"},
|
{"title": "搜索结果 1", "snippet": "找到相关内容,请查看详情"},
|
||||||
|
|
@ -33,26 +23,37 @@ class WebSearchTool(BaseTool):
|
||||||
name = "web_search"
|
name = "web_search"
|
||||||
description = "在互联网上搜索信息,返回相关网页摘要"
|
description = "在互联网上搜索信息,返回相关网页摘要"
|
||||||
parameters = {
|
parameters = {
|
||||||
"query": {
|
"query": {"type": "string", "description": "搜索关键词"},
|
||||||
"type": "string",
|
"max_results": {"type": "integer", "description": "返回结果数量"},
|
||||||
"description": "搜索关键词或问题",
|
|
||||||
},
|
|
||||||
"max_results": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "返回结果数量,默认 3",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def execute(self, query: str, max_results: int = 3, **_) -> ToolResult:
|
def __init__(self):
|
||||||
time.sleep(0.1) # 模拟网络延迟
|
super().__init__()
|
||||||
|
cfg = settings.tools.web_search
|
||||||
|
self._default_max = cfg.max_results
|
||||||
|
self._engine = cfg.engine
|
||||||
|
self._api_key = cfg.api_key
|
||||||
|
self._timeout = cfg.timeout
|
||||||
|
self.logger.debug(
|
||||||
|
f"⚙️ WebSearch engine={self._engine}, "
|
||||||
|
f"max_results={self._default_max}, "
|
||||||
|
f"api_key={'***' if self._api_key else '(未设置)'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, query: str, max_results: int | None = None, **_) -> ToolResult:
|
||||||
|
max_results = max_results or self._default_max
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if self._engine != "mock" and self._api_key:
|
||||||
|
# 生产环境:调用真实搜索 API
|
||||||
|
# results = self._call_real_api(query, max_results)
|
||||||
|
pass
|
||||||
|
|
||||||
# 关键词匹配模拟结果
|
|
||||||
results = _DEFAULT_RESULTS
|
results = _DEFAULT_RESULTS
|
||||||
for keyword, data in _MOCK_RESULTS.items():
|
for kw, data in _MOCK_RESULTS.items():
|
||||||
if keyword in query:
|
if kw in query:
|
||||||
results = data
|
results = data
|
||||||
break
|
break
|
||||||
|
|
||||||
results = results[:max_results]
|
results = results[:max_results]
|
||||||
formatted = "\n".join(
|
formatted = "\n".join(
|
||||||
f"[{i+1}] {r['title']}\n {r['snippet']}"
|
f"[{i+1}] {r['title']}\n {r['snippet']}"
|
||||||
|
|
@ -60,6 +61,6 @@ class WebSearchTool(BaseTool):
|
||||||
)
|
)
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
success=True,
|
success=True,
|
||||||
output=f"搜索「{query}」,共 {len(results)} 条结果:\n{formatted}",
|
output=f"搜索「{query}」({self._engine}),共 {len(results)} 条:\n{formatted}",
|
||||||
metadata={"query": query, "count": len(results)},
|
metadata={"query": query, "engine": self._engine, "count": len(results)},
|
||||||
)
|
)
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
"""日志工具"""
|
|
||||||
"""
|
"""
|
||||||
utils/logger.py
|
utils/logger.py
|
||||||
统一日志模块,支持彩色终端输出与文件记录
|
统一日志模块:从 settings 读取日志级别与文件路径配置
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,7 +21,6 @@ class Color:
|
||||||
GREY = "\033[90m"
|
GREY = "\033[90m"
|
||||||
|
|
||||||
|
|
||||||
# ── 自定义彩色 Formatter ───────────────────────────────────────
|
|
||||||
class ColorFormatter(logging.Formatter):
|
class ColorFormatter(logging.Formatter):
|
||||||
LEVEL_COLORS = {
|
LEVEL_COLORS = {
|
||||||
logging.DEBUG: Color.GREY,
|
logging.DEBUG: Color.GREY,
|
||||||
|
|
@ -32,65 +29,65 @@ class ColorFormatter(logging.Formatter):
|
||||||
logging.ERROR: Color.RED,
|
logging.ERROR: Color.RED,
|
||||||
logging.CRITICAL: Color.MAGENTA,
|
logging.CRITICAL: Color.MAGENTA,
|
||||||
}
|
}
|
||||||
|
|
||||||
COMPONENT_COLORS = {
|
COMPONENT_COLORS = {
|
||||||
"CLIENT": Color.BLUE,
|
"CLIENT": Color.BLUE,
|
||||||
"LLM": Color.GREEN,
|
"LLM": Color.GREEN,
|
||||||
"MCP": Color.YELLOW,
|
"MCP": Color.YELLOW,
|
||||||
"TOOL": Color.MAGENTA,
|
"TOOL": Color.MAGENTA,
|
||||||
"MEMORY": Color.CYAN,
|
"MEMORY": Color.CYAN,
|
||||||
"SYSTEM": Color.GREY,
|
"SYSTEM": Color.GREY,
|
||||||
|
"CONFIG": Color.GREEN,
|
||||||
}
|
}
|
||||||
|
|
||||||
def format(self, record: logging.LogRecord) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
from datetime import datetime
|
||||||
level_color = self.LEVEL_COLORS.get(record.levelno, Color.RESET)
|
level_color = self.LEVEL_COLORS.get(record.levelno, Color.RESET)
|
||||||
time_str = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
time_str = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
|
||||||
# 从 logger 名称提取组件标签,例如 "agent.CLIENT"
|
|
||||||
component = record.name.split(".")[-1].upper()
|
component = record.name.split(".")[-1].upper()
|
||||||
comp_color = self.COMPONENT_COLORS.get(component, Color.RESET)
|
comp_color = self.COMPONENT_COLORS.get(component, Color.RESET)
|
||||||
|
return (
|
||||||
prefix = (
|
|
||||||
f"{Color.GREY}[{time_str}]{Color.RESET} "
|
f"{Color.GREY}[{time_str}]{Color.RESET} "
|
||||||
f"{comp_color}{Color.BOLD}[{component:6s}]{Color.RESET} "
|
f"{comp_color}{Color.BOLD}[{component:6s}]{Color.RESET} "
|
||||||
f"{level_color}{record.getMessage()}{Color.RESET}"
|
f"{level_color}{record.getMessage()}{Color.RESET}"
|
||||||
)
|
)
|
||||||
return prefix
|
|
||||||
|
|
||||||
|
|
||||||
# ── Logger 工厂函数 ────────────────────────────────────────────
|
def get_logger(component: str, level: int | None = None) -> logging.Logger:
|
||||||
def get_logger(component: str, level: int = logging.DEBUG) -> logging.Logger:
|
|
||||||
"""
|
"""
|
||||||
获取指定组件的 Logger 实例。
|
获取指定组件的 Logger 实例,日志级别与文件路径从 settings 读取
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
component: 组件名称,如 "CLIENT"、"LLM"、"MCP"
|
component: 组件名称,如 "CLIENT"、"LLM"、"MCP"
|
||||||
level: 日志级别,默认 DEBUG
|
level: 覆盖日志级别(None 时从 settings 读取)
|
||||||
|
|
||||||
Returns:
|
|
||||||
配置好的 Logger 实例
|
|
||||||
"""
|
"""
|
||||||
|
# 延迟导入避免循环依赖(settings 初始化时也会用到 logger)
|
||||||
|
from config.settings import settings as cfg
|
||||||
|
|
||||||
|
if level is None:
|
||||||
|
level = getattr(logging, cfg.logging.level, logging.DEBUG)
|
||||||
|
|
||||||
logger = logging.getLogger(f"agent.{component}")
|
logger = logging.getLogger(f"agent.{component}")
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
|
||||||
# 避免重复添加 Handler
|
|
||||||
if logger.handlers:
|
if logger.handlers:
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
# 终端 Handler(彩色)
|
# 终端 Handler
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
console_handler.setFormatter(ColorFormatter())
|
console_handler.setFormatter(ColorFormatter())
|
||||||
logger.addHandler(console_handler)
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
# 文件 Handler(纯文本)
|
# 文件 Handler(由配置控制开关)
|
||||||
log_dir = Path("logs")
|
if cfg.logging.enable_file:
|
||||||
log_dir.mkdir(exist_ok=True)
|
log_dir = Path(cfg.logging.log_dir)
|
||||||
file_handler = logging.FileHandler(log_dir / "agent.log", encoding="utf-8")
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
file_handler.setFormatter(
|
file_handler = logging.FileHandler(
|
||||||
logging.Formatter("[%(asctime)s] [%(name)s] %(levelname)s: %(message)s")
|
log_dir / cfg.logging.log_file, encoding="utf-8"
|
||||||
)
|
)
|
||||||
logger.addHandler(file_handler)
|
file_handler.setFormatter(
|
||||||
|
logging.Formatter("[%(asctime)s] [%(name)s] %(levelname)s: %(message)s")
|
||||||
|
)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
# 防止日志向上传播到 root logger
|
|
||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
return logger
|
return logger
|
||||||
Loading…
Reference in New Issue