From 1a09129395bd11f28a48751836cd7fa5dbac3b1c Mon Sep 17 00:00:00 2001 From: liusongtao Date: Sat, 28 Feb 2026 16:21:35 +0800 Subject: [PATCH] first agent project --- client/__init__.py | 1 + client/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 170 bytes .../__pycache__/agent_client.cpython-310.pyc | Bin 0 -> 4911 bytes client/agent_client.py | 158 +++++++++++ llm/__init__.py | 1 + llm/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 167 bytes llm/__pycache__/llm_engine.cpython-310.pyc | Bin 0 -> 7691 bytes llm/llm_engine.py | 248 ++++++++++++++++++ logs/agent.log | 210 +++++++++++++++ main.py | 160 +++++++++++ mcp/__init__.py | 1 + mcp/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 167 bytes mcp/__pycache__/mcp_protocol.cpython-310.pyc | Bin 0 -> 3519 bytes mcp/__pycache__/mcp_server.cpython-310.pyc | Bin 0 -> 5027 bytes mcp/mcp_protocol.py | 107 ++++++++ mcp/mcp_server.py | 143 ++++++++++ memory/__init__.py | 1 + memory/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 170 bytes .../__pycache__/memory_store.cpython-310.pyc | Bin 0 -> 5137 bytes memory/memory_store.py | 128 +++++++++ tools/__init__.py | 1 + tools/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 169 bytes tools/__pycache__/base_tool.cpython-310.pyc | Bin 0 -> 3092 bytes tools/__pycache__/calculator.cpython-310.pyc | Bin 0 -> 2099 bytes .../__pycache__/code_executor.cpython-310.pyc | Bin 0 -> 1949 bytes tools/__pycache__/file_reader.cpython-310.pyc | Bin 0 -> 1882 bytes tools/__pycache__/web_search.cpython-310.pyc | Bin 0 -> 2118 bytes tools/base_tool.py | 82 ++++++ tools/calculator.py | 64 +++++ tools/code_executor.py | 60 +++++ tools/file_reader.py | 65 +++++ tools/web_search.py | 65 +++++ utils/__pycache__/logger.cpython-310.pyc | Bin 0 -> 2424 bytes utils/logger.py | 96 +++++++ 34 files changed, 1591 insertions(+) create mode 100644 client/__init__.py create mode 100644 client/__pycache__/__init__.cpython-310.pyc create mode 100644 client/__pycache__/agent_client.cpython-310.pyc create mode 100644 client/agent_client.py create mode 100644 llm/__init__.py create mode 100644 llm/__pycache__/__init__.cpython-310.pyc create mode 100644 llm/__pycache__/llm_engine.cpython-310.pyc create mode 100644 llm/llm_engine.py create mode 100644 logs/agent.log create mode 100644 main.py create mode 100644 mcp/__init__.py create mode 100644 mcp/__pycache__/__init__.cpython-310.pyc create mode 100644 mcp/__pycache__/mcp_protocol.cpython-310.pyc create mode 100644 mcp/__pycache__/mcp_server.cpython-310.pyc create mode 100644 mcp/mcp_protocol.py create mode 100644 mcp/mcp_server.py create mode 100644 memory/__init__.py create mode 100644 memory/__pycache__/__init__.cpython-310.pyc create mode 100644 memory/__pycache__/memory_store.cpython-310.pyc create mode 100644 memory/memory_store.py create mode 100644 tools/__init__.py create mode 100644 tools/__pycache__/__init__.cpython-310.pyc create mode 100644 tools/__pycache__/base_tool.cpython-310.pyc create mode 100644 tools/__pycache__/calculator.cpython-310.pyc create mode 100644 tools/__pycache__/code_executor.cpython-310.pyc create mode 100644 tools/__pycache__/file_reader.cpython-310.pyc create mode 100644 tools/__pycache__/web_search.cpython-310.pyc create mode 100644 tools/base_tool.py create mode 100644 tools/calculator.py create mode 100644 tools/code_executor.py create mode 100644 tools/file_reader.py create mode 100644 tools/web_search.py create mode 100644 utils/__pycache__/logger.cpython-310.pyc create mode 100644 utils/logger.py diff --git a/client/__init__.py b/client/__init__.py new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/client/__init__.py @@ -0,0 +1 @@ +"""__init__.py""" diff --git a/client/__pycache__/__init__.cpython-310.pyc b/client/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..226faba7037af30cb953dbd783522743b040e9ea GIT binary patch literal 170 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 literal 0 HcmV?d00001 diff --git a/client/agent_client.py b/client/agent_client.py new file mode 100644 index 0000000..802f889 --- /dev/null +++ b/client/agent_client.py @@ -0,0 +1,158 @@ +"""客户端:用户交互 & 会话管理""" +""" +client/agent_client.py +Agent 客户端:协调 LLM 引擎、MCP 服务器、记忆模块,驱动完整 Agent 执行流程 +""" + +from dataclasses import dataclass + +from llm.llm_engine import LLMEngine +from mcp.mcp_protocol import MCPMethod, MCPRequest +from mcp.mcp_server import MCPServer +from memory.memory_store import MemoryStore +from utils.logger import get_logger + + +# ── 单轮执行结果 ─────────────────────────────────────────────── +@dataclass +class AgentResponse: + """一次完整 Agent 调用的结果""" + user_input: str + final_reply: str + tool_used: str | None = None + tool_output: str | None = None + success: bool = True + error: str | None = None + + +# ── Agent 客户端 ─────────────────────────────────────────────── +class AgentClient: + """ + Agent 客户端:实现完整的 ReAct 执行循环 + + 执行流程 (5步): + 1. [CLIENT] 接收用户输入,写入 Memory + 2. [LLM] 分析意图,决策是否调用工具 + 3. [MCP] 构造 JSON-RPC 请求,发送给 MCP Server + 4. [TOOL] MCP Server 执行工具,返回结果 + 5. [LLM] 整合结果,生成最终回复,写入 Memory + + 使用示例: + client = AgentClient(llm=llm, mcp_server=mcp, memory=memory) + response = client.chat("帮我计算 100 * 200") + print(response.final_reply) + """ + + def __init__( + self, + llm: LLMEngine, + mcp_server: MCPServer, + memory: MemoryStore, + ): + self.llm = llm + self.mcp_server = mcp_server + self.memory = memory + self.logger = get_logger("CLIENT") + self.logger.info("💻 Agent Client 初始化完成") + + # ── 主入口 ────────────────────────────────────────────────── + + def chat(self, user_input: str) -> AgentResponse: + """ + 处理一轮用户对话,执行完整 Agent 流程 + + Args: + 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) + context = self.memory.get_context_summary() + + # ── Step 2: LLM 推理决策 ──────────────────────────────── + self.logger.info("🧠 Step 2 [LLM] 开始推理,分析意图...") + tool_schemas = self.mcp_server.get_tool_schemas() + decision = self.llm.think_and_decide(user_input, tool_schemas, context) + + # ── 分支:是否需要工具 ────────────────────────────────── + if not decision.need_tool: + return self._handle_direct_reply(user_input, context) + + return self._handle_tool_call(user_input, decision, 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( + self, + user_input: str, + decision, + context: str, + ) -> AgentResponse: + """执行工具调用的完整流程(Step 3 → 4 → 5)""" + + # ── Step 3: 构造 MCP 请求 ─────────────────────────────── + mcp_request: MCPRequest = decision.to_mcp_request() + self.logger.info( + f"📡 Step 3 [MCP] 发送工具调用请求\n" + f" 方法: {mcp_request.method}\n" + f" 工具: {decision.tool_name}\n" + f" 参数: {decision.arguments}\n" + f" 请求体: {mcp_request.to_dict()}" + ) + + # ── Step 4: MCP Server 执行工具 ───────────────────────── + self.logger.info(f"🔧 Step 4 [TOOL] MCP Server 执行工具 [{decision.tool_name}]...") + mcp_response = self.mcp_server.handle_request(mcp_request) + + if not mcp_response.success: + error_msg = f"工具调用失败: {mcp_response.error}" + self.logger.error(f"❌ {error_msg}") + return AgentResponse( + user_input=user_input, + final_reply=f"抱歉,工具调用失败:{mcp_response.error.get('message')}", + tool_used=decision.tool_name, + success=False, + error=error_msg, + ) + + tool_output = mcp_response.content + self.logger.info(f"✅ 工具执行成功,输出: {tool_output[:80]}...") + self.memory.add_tool_result(decision.tool_name, tool_output) + + # ── Step 5: LLM 整合结果,生成最终回复 ────────────────── + self.logger.info("✍️ Step 5 [LLM] 整合工具结果,生成最终回复...") + final_reply = self.llm.generate_final_reply( + user_input, decision.tool_name, tool_output, context + ) + self.memory.add_assistant_message(final_reply) + + self.logger.info(f"🎉 [CLIENT] 流程完成,回复已返回") + return AgentResponse( + user_input=user_input, + final_reply=final_reply, + tool_used=decision.tool_name, + tool_output=tool_output, + ) + + # ── 工具方法 ──────────────────────────────────────────────── + + def get_memory_stats(self) -> dict: + """获取当前记忆统计""" + return self.memory.stats() + + def clear_session(self) -> None: + """清空当前会话""" + self.memory.clear_history() + self.logger.info("🗑 会话已清空") \ No newline at end of file diff --git a/llm/__init__.py b/llm/__init__.py new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/llm/__init__.py @@ -0,0 +1 @@ +"""__init__.py""" diff --git a/llm/__pycache__/__init__.cpython-310.pyc b/llm/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..232cd1ddf9a4554b6c3abb556c60415eb2a83fcb GIT binary patch literal 167 zcmd1j<>g`kf|=!uGKGQkV-N=!FakLaKwQiLBvKfn7*ZI688n%yxZ~q9^D;}~cq%Ud literal 0 HcmV?d00001 diff --git a/llm/__pycache__/llm_engine.cpython-310.pyc b/llm/__pycache__/llm_engine.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5889d6ed4be98096956d7297da3a30dc8103df4 GIT binary patch literal 7691 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 literal 0 HcmV?d00001 diff --git a/llm/llm_engine.py b/llm/llm_engine.py new file mode 100644 index 0000000..843fc85 --- /dev/null +++ b/llm/llm_engine.py @@ -0,0 +1,248 @@ +"""LLM 引擎:意图理解 & 工具决策""" +""" +llm/llm_engine.py +LLM 引擎:负责意图理解、工具选择决策、最终回复生成 +生产环境可替换 _call_llm_api() 为真实 API 调用(OpenAI / Anthropic 等) +""" + +import json +import re +from dataclasses import dataclass + +from mcp.mcp_protocol import MCPRequest, MCPMethod, ToolSchema +from utils.logger import get_logger +from openai import OpenAI + + +# ── 工具调用决策结果 ─────────────────────────────────────────── +@dataclass +class ToolDecision: + """LLM 决策是否调用工具及调用参数""" + need_tool: bool + tool_name: str = "" + arguments: dict = None + reasoning: str = "" # 推理过程说明 + + def __post_init__(self): + self.arguments = self.arguments or {} + + def to_mcp_request(self) -> MCPRequest | None: + """将工具决策转换为 MCP 请求""" + if not self.need_tool: + return None + return MCPRequest( + method=MCPMethod.TOOLS_CALL, + params={"name": self.tool_name, "arguments": self.arguments}, + ) + +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) + def create(self, model_name, tool_schemas, user_input) -> ToolDecision: + tools = [{ + "name": s.name, + "description": s.description, + "parameters": s.parameters} for s in tool_schemas] + completion = self.client.chat.completions.create( + model=model_name, + functions=tools, + messages = [ + { + "role": "user", + "content": [{ + "type": "text", + "text": user_input + }] + } + ] + ) + 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']) + +# ── LLM 引擎 ────────────────────────────────────────────────── +class LLMEngine: + """ + LLM 推理引擎(ReAct 模式) + + 执行流程: + 1. 接收用户输入 + 工具列表 + 2. 分析意图,决策是否调用工具(think) + 3. 若需要工具,生成 MCPRequest(act) + 4. 接收工具结果,生成最终回复(observe) + + 生产环境替换: + 将 _call_llm_api() 替换为真实 LLM API 调用即可, + 其余流程控制逻辑保持不变。 + """ + + API_KEY = "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR" + def __init__(self, model_name: str = "claude-sonnet-4-6"): + self.model_name = model_name + self.logger = get_logger("LLM") + self.logger.info(f"🧠 LLM 引擎初始化,模型: {model_name}") + self.client = MonicaClient(api_key=self.API_KEY) + + # ── 核心推理流程 ──────────────────────────────────────────── + + def think_and_decide( + self, + user_input: str, + tool_schemas: list[ToolSchema], + context: str = "", + ) -> ToolDecision: + """ + Step 1 & 2: 理解意图,决策工具调用(Think 阶段) + + Args: + user_input: 用户输入文本 + tool_schemas: 可用工具的 Schema 列表 + context: 对话历史上下文摘要 + + Returns: + ToolDecision 实例 + """ + self.logger.info(f"💭 分析意图: {user_input[:50]}...") + + # 构造 Prompt(生产环境发送给真实 LLM) + prompt = self._build_decision_prompt(user_input, tool_schemas, context) + self.logger.debug(f"📝 Prompt 已构造 ({len(prompt)} chars)") + + # 调用 LLM(Demo 中使用规则模拟) + # decision = self._call_llm_api(user_input, tool_schemas) + decision = self._call_llm_api(prompt, tool_schemas) + + self.logger.info( + f"🎯 决策结果: {'调用工具 [' + decision.tool_name + ']' if decision.need_tool else '直接回复'}" + ) + self.logger.debug(f"💡 推理: {decision.reasoning}") + return decision + + def generate_final_reply( + self, + user_input: str, + tool_name: str, + tool_output: str, + context: str = "", + ) -> str: + """ + Step 5: 整合工具结果,生成最终自然语言回复(Observe 阶段) + + Args: + user_input: 原始用户输入 + tool_name: 被调用的工具名称 + tool_output: 工具返回的原始输出 + context: 对话历史上下文 + + Returns: + 最终回复字符串 + """ + self.logger.info("✍️ 整合工具结果,生成最终回复...") + + # 生产环境:将 tool_output 注入 Prompt,调用 LLM 生成回复 + reply = self._synthesize_reply(user_input, tool_name, tool_output) + self.logger.info(f"💬 回复已生成 ({len(reply)} chars)") + return reply + + def generate_direct_reply(self, user_input: str, context: str = "") -> str: + """无需工具时直接生成回复""" + self.logger.info("💬 直接生成回复(无需工具)") + return f"[{self.model_name}] 您好!关于「{user_input}」,这是一个直接回复示例。\n(生产环境此处调用真实 LLM API)" + + # ── Prompt 构造 ───────────────────────────────────────────── + + def _build_decision_prompt( + self, + user_input: str, + tool_schemas: list[ToolSchema], + context: str, + ) -> str: + """构造工具决策 Prompt(ReAct 格式)""" + tools_desc = "\n".join( + f"- {s.name}: {s.description}" for s in tool_schemas + ) + return ( + f"你是一个智能助手,请分析用户输入并决定是否需要调用工具。\n\n" + f"## 可用工具\n{tools_desc}\n\n" + f"## 对话历史\n{context or '(无)'}\n\n" + 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]) -> ToolDecision: + """ + 模拟 LLM API 调用(Demo 版本使用关键词规则) + + 生产环境替换示例: + import anthropic + client = anthropic.Anthropic() + response = client.messages.create( + model=self.model_name, + tools=[s.to_dict() for s in tool_schemas], + messages=[{"role": "user", "content": user_input}] + ) + # 解析 response.content 中的 tool_use block + """ + return self.client.create(self.model_name, user_input=user_input, tool_schemas=tool_schemas) + + text = user_input.lower() + + # 规则匹配:计算器 + calc_pattern = re.search(r"[\d\s\+\-\*\/\(\)\^]+[=??]?", user_input) + if any(kw in text for kw in ["计算", "等于", "多少", "×", "÷"]) and calc_pattern: + expr = re.sub(r"[^0-9+\-*/().**]", "", user_input.replace("×","*").replace("÷","/")) + return ToolDecision( + 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="问题可直接回答,无需工具", + ) + + def _synthesize_reply(self, user_input: str, tool_name: str, tool_output: str) -> str: + """基于工具输出合成最终回复(Demo 版本)""" + return ( + f"✅ 已通过 [{tool_name}] 工具处理您的请求。\n\n" + f"**执行结果:**\n{tool_output}\n\n" + f"---\n*由 {self.model_name} 生成 · 工具: {tool_name}*" + ) \ No newline at end of file diff --git a/logs/agent.log b/logs/agent.log new file mode 100644 index 0000000..00ba484 --- /dev/null +++ b/logs/agent.log @@ -0,0 +1,210 @@ +[2026-02-28 13:16:51,269] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 13:16:51,270] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 13:16:51,272] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6 +[2026-02-28 13:16:51,273] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 13:16:51,273] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 13:16:51,273] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 13:59:08,843] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 13:59:08,844] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 13:59:08,846] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6 +[2026-02-28 13:59:08,847] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 13:59:08,848] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 13:59:08,848] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 13:59:40,111] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 13:59:41,400] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:00:07,071] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:00:51,574] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:02:14,371] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:04:03,060] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:15:21,435] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:15:21,437] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:15:21,437] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:15:21,439] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6 +[2026-02-28 14:15:21,730] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:15:21,731] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:15:21,731] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:15:37,946] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:15:38,634] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1+1等于多少 +[2026-02-28 14:15:39,212] [agent.MEMORY] DEBUG: 💬 [USER] 1+1等于多少... +[2026-02-28 14:15:40,563] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:15:47,714] [agent.LLM] INFO: 💭 分析意图: 1+1等于多少... +[2026-02-28 14:15:50,157] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:17:52,834] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:17:52,836] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:17:52,838] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:17:52,839] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6 +[2026-02-28 14:17:53,009] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:17:53,010] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:17:53,010] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:18:02,052] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:18:02,614] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:18:03,083] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:18:04,093] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:18:07,998] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:18:09,082] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:19:28,781] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:19:28,781] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:19:28,782] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:19:28,782] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:19:28,783] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:19:28,783] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:19:28,783] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:19:28,912] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:19:28,913] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:19:28,913] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:19:38,773] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:19:38,773] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:19:38,774] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:19:38,774] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:19:38,774] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:19:38,774] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:24:25,989] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:24:25,990] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:24:25,992] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:24:26,120] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:24:26,120] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:24:26,121] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:24:47,520] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:24:47,520] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:24:47,521] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:24:47,521] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:24:47,521] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:24:47,521] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:26:41,628] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:26:41,629] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:26:41,630] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:26:41,630] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:26:41,631] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:26:41,631] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:26:41,632] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:26:41,764] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:26:41,764] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:26:41,765] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:26:52,266] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:26:52,266] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:26:52,267] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:26:52,267] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:26:52,267] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:26:52,268] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:29:31,672] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:29:31,673] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:29:31,675] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:29:31,675] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:29:31,809] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:29:31,810] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:29:31,811] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:29:43,546] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:29:43,546] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:29:43,546] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:29:43,547] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:29:43,547] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:29:43,547] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:32:01,347] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:32:01,348] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:32:01,350] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:32:01,351] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:32:01,477] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:32:01,477] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:32:01,478] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:32:08,959] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:32:08,959] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:32:08,959] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:32:08,960] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:32:08,960] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:32:08,960] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:34:04,981] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:34:04,982] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:34:04,983] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:34:04,984] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:34:04,984] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:34:04,985] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:34:04,987] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:34:05,137] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:34:05,140] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:34:05,141] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:34:14,908] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:34:14,909] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:34:14,909] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:35:39,204] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:35:39,205] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:35:39,205] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:35:39,205] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:35:39,206] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:35:39,206] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:35:39,207] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:35:39,333] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:35:39,334] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:35:39,334] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:35:51,583] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:35:51,583] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:35:51,584] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:35:51,584] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:35:51,584] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:35:51,585] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:43:51,069] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统... +[2026-02-28 14:43:51,069] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-02-28 14:43:51,070] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-02-28 14:43:51,070] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-02-28 14:43:51,071] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件 +[2026-02-28 14:43:51,071] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-02-28 14:43:51,071] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o +[2026-02-28 14:43:51,198] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-02-28 14:43:51,198] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成 +[2026-02-28 14:43:51,199] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-02-28 14:44:04,009] [agent.CLIENT] INFO: ======================================================= +[2026-02-28 14:44:04,009] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少 +[2026-02-28 14:44:04,009] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少... +[2026-02-28 14:44:04,010] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图... +[2026-02-28 14:44:04,010] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少... +[2026-02-28 14:44:04,010] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars) +[2026-02-28 14:44:10,159] [agent.LLM] INFO: 🎯 决策结果: 调用工具 [calculator] +[2026-02-28 14:44:10,159] [agent.LLM] DEBUG: 💡 推理: 用户询问数学计算问题,直接调用计算工具得到1+1的结果是最合适的。 +[2026-02-28 14:44:21,419] [agent.CLIENT] INFO: 📡 Step 3 [MCP] 发送工具调用请求 + 方法: tools/call + 工具: calculator + 参数: {'expression': '1+1'} + 请求体: {'jsonrpc': '2.0', 'id': 'e4d8732a', 'method': 'tools/call', 'params': {'name': 'calculator', 'arguments': {'expression': '1+1'}}} +[2026-02-28 14:44:36,340] [agent.CLIENT] INFO: 🔧 Step 4 [TOOL] MCP Server 执行工具 [calculator]... +[2026-02-28 14:44:40,181] [agent.MCP] INFO: 📨 收到请求 id=e4d8732a method=tools/call +[2026-02-28 14:46:24,701] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '1+1'} +[2026-02-28 14:46:50,239] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功 +[2026-02-28 16:05:27,152] [agent.CLIENT] INFO: ✅ 工具执行成功,输出: 1+1 = 2... +[2026-02-28 16:05:46,521] [agent.MEMORY] DEBUG: 💬 [TOOL] 1+1 = 2... +[2026-02-28 16:05:49,469] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 整合工具结果,生成最终回复... +[2026-02-28 16:05:53,270] [agent.LLM] INFO: ✍️ 整合工具结果,生成最终回复... +[2026-02-28 16:05:53,271] [agent.LLM] INFO: 💬 回复已生成 (83 chars) +[2026-02-28 16:06:03,400] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ 已通过 [calculator] 工具处理您的请求。 + +**执行结果:** +1+1 = 2 + +--- +*由 gpt-... +[2026-02-28 16:06:05,167] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回 diff --git a/main.py b/main.py new file mode 100644 index 0000000..48a7a60 --- /dev/null +++ b/main.py @@ -0,0 +1,160 @@ +"""程序入口""" +""" +main.py +智能体 Demo 程序入口 +组装所有模块,启动交互式对话循环 +""" + +import sys + +# ── 导入各模块 ───────────────────────────────────────────────── +from client.agent_client import AgentClient +from llm.llm_engine import LLMEngine +from mcp.mcp_server import MCPServer +from memory.memory_store import MemoryStore +from tools.calculator import CalculatorTool +from tools.code_executor import CodeExecutorTool +from tools.file_reader import FileReaderTool +from tools.web_search import WebSearchTool +from utils.logger import get_logger + +logger = get_logger("SYSTEM") + + +# ── 系统组装 ─────────────────────────────────────────────────── +def build_agent() -> AgentClient: + """ + 工厂函数:组装并返回完整的 Agent 实例 + + 组装顺序: + 1. 初始化 MCP Server,注册所有工具 + 2. 初始化 LLM 引擎 + 3. 初始化 Memory 模块 + 4. 组装 AgentClient + """ + logger.info("🔧 开始组装 Agent 系统...") + + # 1. MCP Server:注册所有工具 + mcp_server = MCPServer(server_name="DemoMCPServer") + mcp_server.register_tools( + CalculatorTool, + WebSearchTool, + FileReaderTool, + CodeExecutorTool, + ) + + # 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) + + logger.info(f"✅ Agent 组装完成,已注册工具: {mcp_server.list_tools()}") + return client + + +# ── 演示场景 ─────────────────────────────────────────────────── +def run_demo(client: AgentClient) -> None: + """运行预设演示场景,展示各工具的完整调用链路""" + demo_cases = [ + ("🔢 数学计算", "计算 (100 + 200) × 3 等于多少"), + ("🌐 网络搜索", "搜索 Python 最新版本的新特性"), + ("📄 文件读取", "读取文件 config.json 的内容"), + ("🐍 代码执行", '执行代码 `print("Hello, Agent!")`'), + ] + + logger.info("\n" + "═" * 60) + logger.info("🎬 开始演示模式,共 4 个场景") + logger.info("═" * 60) + + for title, question in demo_cases: + logger.info(f"\n{'─' * 55}") + logger.info(f"📌 场景: {title}") + logger.info(f"{'─' * 55}") + + response = client.chat(question) + + print(f"\n{'─' * 55}") + print(f"👤 用户: {response.user_input}") + if response.tool_used: + print(f"🔧 工具: {response.tool_used}") + print(f"📤 输出: {response.tool_output[:120]}...") + print(f"🤖 回复:\n{response.final_reply}") + print() + + # 打印记忆统计 + stats = client.get_memory_stats() + logger.info(f"\n📊 Memory 统计: {stats}") + + +# ── 交互式对话循环 ───────────────────────────────────────────── +def run_interactive(client: AgentClient) -> None: + """启动交互式命令行对话""" + print("\n" + "═" * 60) + print(" 🤖 Agent Demo — 交互模式") + print(" 输入 'quit' → 退出程序") + print(" 输入 'clear' → 清空会话历史") + print(" 输入 'stats' → 查看 Memory 统计") + print(" 输入 'tools' → 查看已注册工具列表") + print("═" * 60 + "\n") + + while True: + try: + user_input = input("👤 你: ").strip() + except (KeyboardInterrupt, EOFError): + print("\n👋 再见!") + break + + if not user_input: + continue + + # ── 内置命令 ────────────────────────────────────────── + match user_input.lower(): + case "quit" | "exit": + print("👋 再见!") + break + case "clear": + client.clear_session() + print("✅ 会话已清空\n") + continue + case "stats": + print(f"📊 Memory 统计: {client.get_memory_stats()}\n") + continue + case "tools": + tools = client.mcp_server.list_tools() + print(f"🔧 已注册工具 ({len(tools)} 个): {', '.join(tools)}\n") + continue + + # ── 执行 Agent 完整流程 ─────────────────────────────── + response = client.chat(user_input) + + print(f"\n{'─' * 55}") + if response.tool_used: + print(f" 🔧 调用工具: {response.tool_used}") + print(f"🤖 Agent:\n{response.final_reply}") + print(f"{'─' * 55}\n") + + +# ── 主函数 ───────────────────────────────────────────────────── +def main() -> None: + """ + 主函数入口,支持两种运行模式: + + python main.py → 交互模式(默认) + python main.py demo → 演示模式(自动执行预设场景) + """ + client = build_agent() + + mode = sys.argv[1] if len(sys.argv) > 1 else "interactive" + + if mode == "demo": + run_demo(client) + else: + run_interactive(client) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mcp/__init__.py b/mcp/__init__.py new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/mcp/__init__.py @@ -0,0 +1 @@ +"""__init__.py""" diff --git a/mcp/__pycache__/__init__.cpython-310.pyc b/mcp/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df2f6cca700ef8a9a3c8537bcfa7768a1e69e492 GIT binary patch literal 167 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 dict: + return { + "jsonrpc": self.jsonrpc, + "id": self.id, + "method": self.method, + "params": self.params, + } + + +# ── 响应消息 ─────────────────────────────────────────────────── +@dataclass +class MCPResponse: + """ + 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 + result: dict[str, Any] | None = None + error: dict[str, Any] | None = None + jsonrpc: str = "2.0" + + @property + def success(self) -> bool: + return self.error is None + + @property + def content(self) -> str: + """提取响应中的文本内容""" + if not self.success or not self.result: + return self.error.get("message", "Unknown error") if self.error else "" + items = self.result.get("content", []) + return "\n".join(item.get("text", "") for item in items if item.get("type") == "text") + + def to_dict(self) -> dict: + base = {"jsonrpc": self.jsonrpc, "id": self.id} + if self.success: + base["result"] = self.result + else: + base["error"] = self.error + return base + + +# ── 工具描述 ─────────────────────────────────────────────────── +@dataclass +class ToolSchema: + """ + 工具的元数据描述,用于 LLM 识别和选择工具 + """ + name: str + description: str + parameters: dict[str, Any] # JSON Schema 格式的参数定义 + + def to_dict(self) -> dict: + return { + "name": self.name, + "description": self.description, + "inputSchema": { + "type": "object", + "properties": self.parameters, + }, + } \ No newline at end of file diff --git a/mcp/mcp_server.py b/mcp/mcp_server.py new file mode 100644 index 0000000..e0734f3 --- /dev/null +++ b/mcp/mcp_server.py @@ -0,0 +1,143 @@ +"""MCP 服务器:工具注册 & 调度""" +""" +mcp/mcp_server.py +MCP Server:工具注册中心与调度引擎 +负责管理所有工具的生命周期,处理 JSON-RPC 格式的工具调用请求 +""" + +import json +from typing import Type + +from mcp.mcp_protocol import MCPMethod, MCPRequest, MCPResponse, ToolSchema +from tools.base_tool import BaseTool, ToolResult +from utils.logger import get_logger + + +class MCPServer: + """ + MCP 服务器核心类 + + 职责: + 1. 工具注册(register_tool) + 2. 工具列表查询(tools/list) + 3. 工具调用分发(tools/call) + 4. JSON-RPC 协议封装/解析 + + 使用示例: + server = MCPServer() + server.register_tool(CalculatorTool) + response = server.handle_request(request) + """ + + def __init__(self, server_name: str = "AgentMCPServer"): + self.server_name = server_name + self.logger = get_logger("MCP") + self._registry: dict[str, BaseTool] = {} # 工具名 → 工具实例 + + self.logger.info(f"🚀 MCP Server [{server_name}] 启动") + + # ── 工具注册 ──────────────────────────────────────────────── + + def register_tool(self, tool_class: Type[BaseTool]) -> None: + """ + 注册一个工具类到服务器 + + Args: + tool_class: 继承自 BaseTool 的工具类(传入类本身,不是实例) + """ + instance = tool_class() + if not instance.name: + raise ValueError(f"工具类 {tool_class.__name__} 未设置 name 属性") + + self._registry[instance.name] = instance + self.logger.info(f"📌 注册工具: [{instance.name}] — {instance.description}") + + def register_tools(self, *tool_classes: Type[BaseTool]) -> None: + """批量注册多个工具类""" + for cls in tool_classes: + self.register_tool(cls) + + # ── 请求处理入口 ──────────────────────────────────────────── + + def handle_request(self, request: MCPRequest) -> MCPResponse: + """ + 处理 MCP 请求的统一入口,根据 method 分发到对应处理器 + + Args: + request: MCPRequest 实例 + + Returns: + MCPResponse 实例 + """ + self.logger.info(f"📨 收到请求 id={request.id} method={request.method}") + + handlers = { + MCPMethod.TOOLS_LIST: self._handle_tools_list, + MCPMethod.TOOLS_CALL: self._handle_tools_call, + } + + handler = handlers.get(request.method) + if handler is None: + return self._error_response(request.id, -32601, f"未知方法: {request.method}") + + return handler(request) + + # ── 私有处理器 ────────────────────────────────────────────── + + def _handle_tools_list(self, request: MCPRequest) -> MCPResponse: + """处理 tools/list 请求,返回所有已注册工具的 Schema""" + schemas = [tool.get_schema().to_dict() for tool in self._registry.values()] + self.logger.info(f"📋 返回工具列表,共 {len(schemas)} 个工具") + return MCPResponse( + id=request.id, + result={"tools": schemas}, + ) + + def _handle_tools_call(self, request: MCPRequest) -> MCPResponse: + """处理 tools/call 请求,调用指定工具并返回结果""" + tool_name = request.params.get("name") + arguments = request.params.get("arguments", {}) + + # 检查工具是否存在 + tool = self._registry.get(tool_name) + if tool is None: + available = list(self._registry.keys()) + return self._error_response( + request.id, -32602, + f"工具 [{tool_name}] 不存在,可用工具: {available}" + ) + + # 执行工具 + result: ToolResult = tool.safe_execute(**arguments) + + if result.success: + return MCPResponse( + id=request.id, + result={ + "content": [{"type": "text", "text": result.output}], + "metadata": result.metadata, + }, + ) + else: + return self._error_response(request.id, -32000, result.output) + + # ── 工具方法 ──────────────────────────────────────────────── + + def get_tool_schemas(self) -> list[ToolSchema]: + """获取所有工具的 Schema 列表(供 LLM 引擎使用)""" + return [tool.get_schema() for tool in self._registry.values()] + + def list_tools(self) -> list[str]: + """返回所有已注册工具的名称列表""" + return list(self._registry.keys()) + + @staticmethod + def _error_response(req_id: str, code: int, message: str) -> MCPResponse: + """构造标准 JSON-RPC 错误响应""" + return MCPResponse( + id=req_id, + error={"code": code, "message": message}, + ) + + def __repr__(self) -> str: + return f"MCPServer(name={self.server_name!r}, tools={self.list_tools()})" \ No newline at end of file diff --git a/memory/__init__.py b/memory/__init__.py new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/memory/__init__.py @@ -0,0 +1 @@ +"""__init__.py""" diff --git a/memory/__pycache__/__init__.cpython-310.pyc b/memory/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..099cc7661127175d53a7d4af084b3cfa1d02e6e9 GIT binary patch literal 170 zcmd1j<>g`kf|=!uGKGQkV-N=!FakLaKwQiLBvKfn7*ZI688n%yxZ~q9^D;}~L{Vi7l6ki{Y;yBcN^?@}K(-fy%;R7H04Y!_h5!Hn literal 0 HcmV?d00001 diff --git a/memory/__pycache__/memory_store.cpython-310.pyc b/memory/__pycache__/memory_store.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79975c8b5236caa8c462b7af9cfed356fc901f81 GIT binary patch literal 5137 zcmai2TXP)66`q-$oxSQ}Sr!Nkj0H}-fox33RWXhYp~!=^Avjc$wW+CPd!($h7rAG) zf;VyskOlHZ7zba7ZCR3wi=D!j{QR?yO#=m+zmLO2~3eJvj;MAK1OGSwF)7GmLJ5(|$iqjEZiafC%N zj*cqnIE(F<67ljjyl{E;mb>(}cWuUi6<(rf4yKlRvKda5o)ZTl01wv9n zc7n`2Ts9Mm6JZ%Ulg(!=%LxwV%sj)*Fy>46^Kc=Q*tPO^?JD~y7vNA(vxeM=dC(e13GrpnTKgPW`ZzckcTDDB? zL||0TvNJ{7QSDMG@2I?#H=R(nRJ6^aeP0`%CmY()*wVB-BTXwzo{{lZB~=M;|9SuR zf&T6L69FECgcB&1{@{cyn-7w&u1mZVv!eO3|M~uduRsUupoN!}4*t5te{T(EvgSeP zU9=5m7D@*TX!w!(Kf|(1+)NH1al*UunZj!stZs(iBON)ZMe|~ZqtGxehvgo*OYTy{y$z6{H}9bb{)MIiYXUXvV*oaw zsj}cPL^9J%jbe0A6_Kf?LrpV2e8a2_GVOIajdie2fI7l9tv=bsHe)8r9%tPMn(%W7 z4>M-QdnK&8h4tV)ZDK~ljKUsZ4??;fGa5#{>>-Rg*c0qwwiUfj%*QeR2zwNxP3$qV ztFE8Uwqa&73uBYFJFx+Q!u^E4^2Y#1RL9ScC!bB+8y~oDU5kqO+SCp2@{~W{uUGFZ z)y`hi1(oR4rAbJ7m)}@kI-{rb)!U!c-o5Ew9d%FN^e!!V6IZMA zx83*7th{sDJ9o>Qn00Tze{b=)udc6P?ZeyEyC?g_tH?M%?(}E%CewEmGOruI16v@D zo_%^}qD45FVay;>F%V$HJv;iUcc$Hs?)2SPa+P>m#qTY?>0LfvU3jDV`8W)yE_~$9 z&tj!}i&IT2)%AdezN$$g-umPY|Ai|Qk4~&f>nt%v<41ksC=uZO=-8m*QG~Z4k5H@F zfe~b-3F@mu2z?_+;Xkc0%wLk2IvJRek%fZ5h*tg$7Fc!2UOr^4`BM0E&2{~HGeQ7~ z+PS~fE*=9a+z;zJ4z|WdtuE(i11}8x;^p6h&oo$*H;d(O;B{Y2{rPTve{^?z(*5M5 zd+I!(PS z>;k;62p<#Ksv?(BT-`Z`kn+ao{P5rrJab)HHQsk@V!7PYp} z+cw0D3HE{I-$td?k?p7si&C&I0|bX%AckmP(b1roKFL4VlLsyS6e~&z`i{ z%c2@^;oFp@P^Ol$nY^_doyK!GY^XHgJMe5Hcz%L@8pWzes#fb;V@*r`ie5_v zTr{f6?f9!a@HH=A=K?BqNm&6NBB%y<01mnQC?1&Y0y1X{sA6ALFA3Uw+3EOC;>;cPd~2Ce+Cd$?_Sag-!E}&on5ckRwyN5$I1EY zoJEcz7Yn=)dkegDLmjBXcW|Nt?rjZ?%Zeo14Pyo zXHGDiH#1ytH7D-8FNWKMP@_;PK}_&;ROyr}z0LNmliOe!QH?+|>DGud@gH9;U@3Bw zf3y@E_ybX}B~&MXN>NzoN9Ge>XIc>F zVcW9FBtmvL5cUtHo0`oMGES&YL+g@HBP4`fR7v|g)|zn-snfwmXdKd*!~%X`ppp)X z9Ystff~hhA2es{|`T)OX0ZaSEe?$#sR{mpZMB(uw^{B>cq}gB6#QJK3 zQ?2?ZTz-a?lQMlXpbreU)KJKa3RS4pq^eL_oW=VfLeES$M^;r3upl#^U4(dH`_%a-zaGb=s;_~m_U_4^gVmqN?`BoIW;(d;9` zx#E!j{UBbpb9pOSKOclhk^mvrzn_Bm4;*iAtzvZpF3ay;0HlK}n{MeUTkI{ydI CeZ~d= literal 0 HcmV?d00001 diff --git a/memory/memory_store.py b/memory/memory_store.py new file mode 100644 index 0000000..9f521d9 --- /dev/null +++ b/memory/memory_store.py @@ -0,0 +1,128 @@ +"""记忆模块:对话历史管理""" +""" +memory/memory_store.py +Agent 记忆模块:管理对话历史(短期记忆)与关键信息摘要(长期记忆) +""" + +from collections import deque +from dataclasses import dataclass, field +from datetime import datetime +from typing import Literal + +from utils.logger import get_logger + + +# ── 消息数据结构 ─────────────────────────────────────────────── +@dataclass +class Message: + """单条对话消息""" + role: Literal["user", "assistant", "tool"] + content: str + timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S")) + metadata: dict = field(default_factory=dict) + + def to_dict(self) -> dict: + return { + "role": self.role, + "content": self.content, + "timestamp": self.timestamp, + } + + +# ── 记忆存储 ─────────────────────────────────────────────────── +class MemoryStore: + """ + 对话记忆存储 + + 短期记忆: 使用 deque 保存最近 N 轮对话,自动滚动淘汰旧消息 + 长期记忆: 保存关键事实摘要(生产环境可替换为向量数据库) + + 使用示例: + memory = MemoryStore(max_history=10) + memory.add_user_message("你好") + memory.add_assistant_message("你好!有什么可以帮你?") + history = memory.get_history() + """ + + def __init__(self, max_history: int = 20): + """ + Args: + max_history: 短期记忆保留的最大消息条数 + """ + self.logger = get_logger("MEMORY") + self.max_history = max_history + self._history: deque[Message] = deque(maxlen=max_history) + self._facts: list[str] = [] # 长期记忆:关键事实 + + self.logger.info(f"💾 Memory 初始化,最大历史: {max_history} 条") + + # ── 写入接口 ──────────────────────────────────────────────── + + def add_user_message(self, content: str) -> None: + """记录用户消息""" + self._add(Message(role="user", content=content)) + + def add_assistant_message(self, content: str) -> None: + """记录 Agent 回复""" + self._add(Message(role="assistant", content=content)) + + def add_tool_result(self, tool_name: str, result: str) -> None: + """记录工具调用结果""" + self._add(Message( + role="tool", + content=result, + metadata={"tool": tool_name}, + )) + + def add_fact(self, fact: str) -> None: + """向长期记忆中添加关键事实""" + self._facts.append(fact) + self.logger.debug(f"📌 长期记忆新增: {fact}") + + # ── 读取接口 ──────────────────────────────────────────────── + + def get_history(self, last_n: int | None = None) -> list[dict]: + """ + 获取对话历史(LLM 上下文格式) + + Args: + last_n: 仅返回最近 N 条,None 表示全部 + + Returns: + 消息字典列表,格式: [{"role": ..., "content": ...}, ...] + """ + messages = list(self._history) + if last_n: + messages = messages[-last_n:] + return [m.to_dict() for m in messages] + + def get_facts(self) -> list[str]: + """获取所有长期记忆事实""" + return list(self._facts) + + def get_context_summary(self) -> str: + """生成上下文摘要字符串,供 LLM Prompt 使用""" + history = self.get_history(last_n=6) + lines = [f"[{m['role'].upper()}] {m['content'][:80]}" for m in history] + return "\n".join(lines) if lines else "(暂无对话历史)" + + # ── 管理接口 ──────────────────────────────────────────────── + + def clear_history(self) -> None: + """清空短期对话历史""" + self._history.clear() + self.logger.info("🗑 对话历史已清空") + + def stats(self) -> dict: + """返回记忆统计信息""" + return { + "history_count": len(self._history), + "facts_count": len(self._facts), + "max_history": self.max_history, + } + + # ── 私有方法 ──────────────────────────────────────────────── + + def _add(self, message: Message) -> None: + self._history.append(message) + self.logger.debug(f"💬 [{message.role.upper()}] {message.content[:60]}...") \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1 @@ +"""__init__.py""" diff --git a/tools/__pycache__/__init__.cpython-310.pyc b/tools/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8aa8febca133fe764e42cb6a8b3d959c435d7ce GIT binary patch literal 169 zcmd1j<>g`kf|=!uGKGQkV-N=!FakLaKwQiLBvKfn7*ZI688n%yxZ~q9^D;}~de@yqL%(h(IH+3ueqzh6Ga?6bDHz33Qto2Rcs0K`Ga6Cf*gYN%sxd zFz#4Z!G7_8Zc`11Tg!d%VB~0UX~e&C-5-%q~8-d&-}>uslCf zS-iSDcQhEi?vGuo-WaN$8jRluaY9z~q#dv9^iui(+hMxl6shxpWoC)jX6lY^6m;7L zLfTR4gHL=9Z27M70WIoC^-@PGSiQZJ87Ql{NkF^|ue}{QD&z=wO56*+$w^eE5mW3G zXcWFur(nvt7*&93>k*#-ZR6J9$F-4WrSP?q!X z)gZdCJL$?2mVlM-i6`5%(DS{(Qb7Lk($0?Vy*6cbw{4Xit9zftKD5iaLA&){T5>d# z7Oid`uI}b=dF`53wroesm-3FLJ&GHn25Hj&IJ!49b88DzHsL9nR?>@9)4aH*6)n>( z;5eyiAGvy=wi44c(=uRMJ^+HeRhp)kN|vKLd8=f@v;-mMrg*ZMHyjp$Z5bNP4QlFE z;hX{;8fqt@iwm--5QVHINs+JoBjOEQIKbW-aZ}7e%xRQITZCxiSnd?^LQbJ=K*f3F zqfL=d(4?u*_8yTVK?YQ$fL;Z3!b}dyxpZx{-AnkM#JCKs0Pecr)ma3c;C^{qbKep6kK?8-XwV4U!aZJU}Y_Hhhn8ZvO+q_)BG zWsDi}RF}REzM2kBd;u9AD%{Fm))@$OAPrIECz*T+vOUuQBd4!SVUU5K=S#hrEo!Do zG%i%{o%M&W`^Sgm4f8SUx2T&pe|Sh|y>?c8VY|ALsjU{`uZ%h_ zD>bofs)2Fa9_G?5YKX{fJsE#;sB-3b2w>1S6#xG7+=S=%4`j03F`nSb=r}LYq`wzm zG2Hl1Kx6tkP;>?fz!^#qB#GGN$$NJ1elshw7_55Huz)d4@`n(Bm1@& zJ+=VRaMg>37PshukVd_*XxoSlli=>|uP|)HJFH+~SnnTQtls{xJa21@_(+^D+ZCUtr4R_!o=RJwIOnEJ_|p7$5VS7g7# zBlnQ_l~Ajo4&;18{Vwh>@MJp};y#{G*|LJtB$)VFrNt%e7b8jOuqmu9jOdlLBA2JD`K zE-SMrj$3E{HU9j7v-xq?3BTBQF%U(W;1m6`|B;3jco2qsErwv&@gJT8U%>zmmv4y4 zO}QwT;t_IDxGYZ)BJ{80rX}FuA(Ld344|pVE5TO;Uiq?wd`HB6kj1j#$bD}RVL*aE zH*iikHfx;GBT`+e1IMVTa2g8fY4|rPya(5c0qIk4VfZ~n4&mkE2I`9F#pcg_=ifc% z&&>s=&s9h9QsGaH_`^4A`3gm2>>A`(WqdYtIAtGGrdDSLE8{o+cQ)bOWOBCp)6>gK6TxTG4ei`DpzCj6-d3C8qrUGVg!iLVp8p*x zpZM+4a19o~f%Ra2?YH2>8UN(fkmvHL6&^ef>pY=;9qKL@(=g1k+NRt+9V#S0ToMI_ zVXna;#-0Jflk=q>E6lQIk#2kKut9lQ_ac-ri}7O=vJt_+h$ra>42A`QZw1x9e*dI4LnL`oIMLuF<2rfiqEX%ouU2N3V;NR~Yvd(e uCtt9))`~h-_)urusYySsE-L9Mo#AC_J60VVeIpC$NRq%KCJAb~HvTvMVw>;) literal 0 HcmV?d00001 diff --git a/tools/__pycache__/calculator.cpython-310.pyc b/tools/__pycache__/calculator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7b5b4fe155dc222e094a47665612f453d88593e GIT binary patch literal 2099 zcmZ`)>u*#=6rb09>~`DoD9|e4`iNz#X#hWv;3GgqKNwm+9ks6YcDs32&l55$Cr-+%=0|Cq0I`}pK%jf!XPO;) zXZ`5+r8~ggGs5zRfv^MKU^^@vNE4MF@63$z~9&&^`lmeOt+5j{Iv=L|%&}N`3fUX3( z3g~K}EkM_locvl›Y)&a&{o|&w=iF&^7Q3=+^T_xK)#G>hj9Pu^$>A;tGr95nm%rApURs>JymU63wtdr< z*S6+5-e}LxfBsdreQQU@md)*uWu``JLN{;`6lJNVVtZ1kP*#fs>^*#JU<@cefHxR{ zWo&~&LpX{HK10T7gf5bZ4$)aW&J-j*!snsn5j&5*1wMi@Tp-+@p$AYeVn|uaR?Yw& zeG{W`fX7%foN>4tS+Jk*)C@U*Z1fR;Yuq|)^**4(co^l<-I|F*UO9&~tN2{>jnVAe z*&LB3L~TP-046!Aogge0Aw$~sLsbivb}K@8+*96rp#e=&grzg*7O!95Zq6H}G=R*x zntFlKR?U-v(CLD4SEzz7%?|o}R8r$9{h?P5#cnBmsZ)KB#Dra$7t(*Xbf{!X6RUEz zW`QUVBmv?Aq+ALi8;ubvMvA{6xgPjGw{>mr-yaAW^ap-b`TYlc`AJaoilW~e6jfE= zqT=@}=!BsEF`hbWquLQ8q8KXi3RnqRVIjB)XE1?Z2D{k(yT#gw-3fAkZ0(c4?0j%g zGjq8o>g|FVlh7_>vjrm*VW=8Zoa3`du49OkHbe+ik0=rmMHL$$Q$)666hUiaQ)4t_ zaSFTlKHLqIwIND@T5ZT?aGH)YQ~wAJ*%uw&El!pBlyd37yKQ5+8_8~xr~X2+u; zlSBqK8|6x4)9v!Bf%2+Krvb1rTNIj>Kw%A(eGka4QnjZBP0+J<_r9HdJ^Ok!9TZ9$ zfXFqmjY6U7RYaj6R~ymUU-jf@Pc7$4gGKp*QQ|}LMI)Gt$d`?V3{>T-Ml#q>7IUxI z#%5pwgJwcDVTQ^5PO<`k+=AC)@>kYM_$J_q)q=;`o&xpbcJ0n>kY*@C^aU6dVHtwd znL^>y&?_e)1M(qsnTj_-yMv&h1C!*?ot$eo&N@tTmkn&MGs_+yPJn04_Q&U{Vo<(rd{G$L@feXCV-ZjzJAjL?zbqwja^k$0 z|6DVi;=H(snY=*%5zPK!_uUk<`J*5EV^z8}Bh87Nqn$#5`$edZDY)DQi*AmmNasQL le=QhR8Vn}km_z^NU%PK58GOfpHpsxj5Mi$6;uW}s{tM(OWB>pF literal 0 HcmV?d00001 diff --git a/tools/__pycache__/code_executor.cpython-310.pyc b/tools/__pycache__/code_executor.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c69976ca9c348970015839f04d5d1667fb8e376a GIT binary patch literal 1949 zcmZ9N?{5@E7{_O3f85^nu05cI0tHT#$Z67q7m8^J;YYv_O*OWp@zP|o-FaFU?{3f8 zS!kQ9(G)BgP$MLWt=b}pz7V2OAVTz?@YTGP>yKBw2LYeitLAX`=JV{#&OY-z&wO_; z%;ho!kHLw{!Id;2f9X!^VPa<=w&(zYlM+c2PB|;n5=|^CvBVlB1FcauD^|&>*d?2i zR|z+{b&_xktZAc^;%tBv?RYIZx_9T>rTGsR&sTM)%%>Fc|#M+!Uputm2KKrk4h1Hm9z$>fG!c#d(KrxNSTv62-Ko(B6oTXfp+9t=s>Q6JhBtkAiH2Kay_g=Zh%f?H*_I4!usRqBg=RIE&de-c|MmXev&(;6 zXv}`SI6u>v`Dpp~^NpF`lpRSCRL9~?=xzDC)xs}*dGcuwSbbAY)IhayhYtB!#(%1~8CGB8>WJ*fFCCZ5v< zB~@n^iFTZzl<4zqY)xNvs~ca6PBLZJ&H3~KsqN<0R7z&FQtLK%W=)>1r)J4lCePIE z09AU<7$O7cRklq8luS94Om^Y?EYHbo-I=DxaN zy2aCI2>Wj;qXu>-i>OW55vvf;7|#NB?>(ne(Kfltxc#@^E&`VbQJAGIMktx zWf`Lxys{tnJjq*G%J9rnn>vr$trw{C0edpp{r?Y3@2ac*A{7PnRNNwVp-~yvt%N)- zW5!wu@5NrZnZGi%6;3j#<_YXz@9IX{^`SpwX?d zQA2-;#Ab~ijV&5mHMVK=YUu3|uW0BxwqUpJ>OUsEVl;wqh)GS#3`Whz%+ju*#=6rZ`Ty?b~2fFKf4v)L%g8d@+hhBO929)6IthSpSS`Owok!XsiAo^HF=d+fOEb#GszJw~z|XX3pF*XXeZ~zw?{f zpryq@(E7%{EB%>9=y#=TZU!g^p!5A82r8m9BA94pT+xaeR^NJAuNXz6Virw|s78!Q zT(n+C#3a@fA{I3!^4~9& zChgZePWytOEFC3}Qy!MZc&2mDNWjkUn&(qr?bzH>pd5hC-v&WZ5ksgN!9|^DMT6+X zAm*F~qZN;-Ijv}tEhJ4X;y};P)SyMOq-Dx1+B6OG4#|+#L4&lB_9>MRXpq2G*?P28 zrpKvAD2oHk41(^mcp`ii&Q9`LoBJV}`!UHh`fxg0y!*$>#nt8M^>04QJxpRxt}(k9 z{ru(XoePa`r;^3e;)0c`r^6S(*9KCzZ$!UL!T!eVo#@8h)um4!cLAUoVys4GhEVRa zQcaYCYPj>EkH*TS#{4<(vA+CubMZ<_x%lh)SBqV_W?Y5lt?B6eQZ#+8xp=2}aWa~{2_U9;==+p& zX$7IEh2lDnu?ua0CMjX=M5q8enM=6P>lsw>62#V3@I_0Vw4BvvsCK99_eu5F~LVlVu-2h zE3G&=F1NToFjY{O&`RJc=E}69t}E!MfLMelAQ|DRpk%72_XuMFE2O|e%Y&Opfn90A z5vM6E_k*gSRUvgc>R(6fIXLg$zC&FDZ*j`_00(O@(Es4t4tS?wZG8NIfSb(+HeH|2 z+L%nyQR;_+=D`hDB8#*7cAUkw=4hIh#Txut@m6f#Z#AFA_6OQO{$Q=Zl$uk*txfJZ!%jh zSJaGO_Ci8C0Iw<)9WQmfoLFO!z=@-MRb-J>_!hF@Gq3(wT5r77k}y zKy(*$X(b`CCqS3Z2rEIwRfP*UsiCS-TqyRu`g#h)KImLgv|Tq8#+q|~JogB$kC6xb zsdIsqw(C~C3UyuSxNapNA-svM%)0Kop;z9p7;0qj7XvXtfS?EfggprpF-zGlWy&fN zmV7V{1%zqJsNze)eI#%b4BMb{wZcXQhR}5l8-Hu2lGa~4GTQbXn5lK$1zVa9ycQr_e3|98H$UrAEkugJv}6b90)tl5rjW1Idj DBJDie literal 0 HcmV?d00001 diff --git a/tools/__pycache__/web_search.cpython-310.pyc b/tools/__pycache__/web_search.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81f3f95774a9b3be95d943b9809b552937c610ad GIT binary patch literal 2118 zcmZuy>2DK96rVl4wi6O=sCsOrO0Cr@B1tJKN-5=NKhUNXN0Hp1mGMl1i|uvxCNklT@T72N0=$#(a%;?ft}eP($B%lL9JZ&D%F`-ZgK2Z>*P>hY*aK zmchhlz|FD6%R_?2W|-zNAbSu-Q4~a&!+es9a_|+Bd`gH4DKRQ?2#Z+4a*oFVtgvqr zkApZ=l7w;Df}}jhM{Ds2M^jurJ+dtvV-UYdHVaz!-;o4^oZF|J-*2gY4lKbloz8{3YVEppA3{dg+!HQO|%_wz;=?P$Guq!hvfKb^0JR9ja6E6-07&dpnG=BkNp&RfB3~(H>_*T&p zZioYD8`@Xaz_&`Ra#qT6KOtjvR&IsIAS#nl9%56hO z!BcC%f;+%+mg`42U@2?|I3*kCM^{1WOK=cwDRfqVn+Vr8_4Tqb^*QB2aSpOt94vK~ zuE{35ly;Ow`&L16Tq_TWKObS%b`UIm7|pevv6-K_wzI6QJO5DaGFYna)d9abH-;j7>H4V@*X^_g zX9}2hJWfoL%DQc3Y>O%>V#RRGiba9|!9yIO z5&WXa##-hpuvi3Gm@1GECkmCE!in(X=gY(pC-cfYFY??Pu8foaIkNJ#MXZl-MkSa> zLR8VTbSy+jv{*W=TQT-jnBeAJX29aVE-|Xq`3K&_KySVcL`J-% WNKjE(fqE(uqi;M@!cgJ)74pB^n(Ffa literal 0 HcmV?d00001 diff --git a/tools/base_tool.py b/tools/base_tool.py new file mode 100644 index 0000000..ef4f48a --- /dev/null +++ b/tools/base_tool.py @@ -0,0 +1,82 @@ +""" +tools/base_tool.py +所有工具的抽象基类,定义统一接口规范 +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +from mcp.mcp_protocol import ToolSchema +from utils.logger import get_logger + + +@dataclass +class ToolResult: + """工具执行结果的统一封装""" + success: bool + output: str + metadata: dict[str, Any] = None + + def __post_init__(self): + self.metadata = self.metadata or {} + + +class BaseTool(ABC): + """ + 工具基类:所有具体工具必须继承此类并实现 execute() 方法 + + 子类示例: + class MyTool(BaseTool): + name = "my_tool" + description = "这是我的工具" + parameters = {"input": {"type": "string", "description": "输入内容"}} + + def execute(self, **kwargs) -> ToolResult: + return ToolResult(success=True, output=f"处理结果: {kwargs['input']}") + """ + + # 子类必须定义的类属性 + name: str = "" + description: str = "" + parameters: dict[str, Any] = {} + + def __init__(self): + self.logger = get_logger("TOOL") + + @abstractmethod + def execute(self, **kwargs) -> ToolResult: + """ + 执行工具逻辑(子类必须实现) + + Args: + **kwargs: 工具参数,与 parameters 中定义的字段对应 + + Returns: + ToolResult 实例 + """ + ... + + def get_schema(self) -> ToolSchema: + """返回工具的 MCP Schema 描述""" + return ToolSchema( + name=self.name, + description=self.description, + parameters=self.parameters, + ) + + def safe_execute(self, **kwargs) -> ToolResult: + """ + 带异常捕获的安全执行入口,由 MCP Server 调用 + + Returns: + ToolResult,失败时 success=False 并携带错误信息 + """ + self.logger.info(f"▶ 执行工具 [{self.name}],参数: {kwargs}") + try: + result = self.execute(**kwargs) + self.logger.info(f"✅ 工具 [{self.name}] 执行成功") + return result + except Exception as exc: + self.logger.error(f"❌ 工具 [{self.name}] 执行失败: {exc}") + return ToolResult(success=False, output=f"工具执行异常: {exc}") \ No newline at end of file diff --git a/tools/calculator.py b/tools/calculator.py new file mode 100644 index 0000000..76e5d4f --- /dev/null +++ b/tools/calculator.py @@ -0,0 +1,64 @@ +"""计算器工具""" +# ════════════════════════════════════════════════════════════════ +# tools/calculator.py — 数学计算工具 +# ════════════════════════════════════════════════════════════════ +""" +tools/calculator.py +安全的数学表达式计算工具(使用 ast 模块避免 eval 注入风险) +""" + +import ast +import operator +from tools.base_tool import BaseTool, ToolResult + + +class CalculatorTool(BaseTool): + name = "calculator" + description = "计算数学表达式,支持加减乘除、幂运算、括号等" + parameters = { + "expression": { + "type": "string", + "description": "数学表达式,例如 '(1+2)*3' 或 '2**10'", + } + } + + # 允许的运算符白名单(防止注入) + _OPERATORS = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Pow: operator.pow, + ast.Mod: operator.mod, + ast.USub: operator.neg, + } + + def execute(self, expression: str, **_) -> ToolResult: + try: + tree = ast.parse(expression, mode="eval") + result = self._eval_node(tree.body) + return ToolResult( + success=True, + output=f"{expression} = {result}", + metadata={"expression": expression, "result": result}, + ) + except (ValueError, TypeError, ZeroDivisionError) as exc: + return ToolResult(success=False, output=f"计算错误: {exc}") + + def _eval_node(self, node: ast.AST) -> float: + """递归解析 AST 节点""" + match node: + case ast.Constant(value=v) if isinstance(v, (int, float)): + return v + case ast.BinOp(left=left, op=op, right=right): + fn = self._OPERATORS.get(type(op)) + if fn is None: + raise ValueError(f"不支持的运算符: {type(op).__name__}") + return fn(self._eval_node(left), self._eval_node(right)) + case ast.UnaryOp(op=op, operand=operand): + fn = self._OPERATORS.get(type(op)) + if fn is None: + raise ValueError(f"不支持的一元运算符: {type(op).__name__}") + return fn(self._eval_node(operand)) + case _: + raise ValueError(f"不支持的表达式节点: {type(node).__name__}") \ No newline at end of file diff --git a/tools/code_executor.py b/tools/code_executor.py new file mode 100644 index 0000000..11be540 --- /dev/null +++ b/tools/code_executor.py @@ -0,0 +1,60 @@ +"""代码执行工具""" + +# ════════════════════════════════════════════════════════════════ +# tools/code_executor.py — 代码执行工具 +# ════════════════════════════════════════════════════════════════ +""" +tools/code_executor.py +沙箱代码执行工具:在受限环境中运行 Python 代码片段 +""" + +import io +import contextlib +import time +from tools.base_tool import BaseTool, ToolResult + + +class CodeExecutorTool(BaseTool): + name = "code_executor" + description = "在沙箱环境中执行 Python 代码片段,返回标准输出" + parameters = { + "code": { + "type": "string", + "description": "要执行的 Python 代码", + }, + "timeout": { + "type": "integer", + "description": "超时时间(秒),默认 5", + }, + } + + # 沙箱:仅允许安全的内置函数 + _SAFE_BUILTINS = { + "print": print, "range": range, "len": len, + "int": int, "float": float, "str": str, "list": list, + "dict": dict, "tuple": tuple, "set": set, "bool": bool, + "abs": abs, "max": max, "min": min, "sum": sum, + "enumerate": enumerate, "zip": zip, "map": map, + "sorted": sorted, "reversed": reversed, + } + + def execute(self, code: str, timeout: int = 5, **_) -> ToolResult: + stdout_buf = io.StringIO() + start_time = time.perf_counter() + + try: + # 重定向 stdout,捕获 print 输出 + with contextlib.redirect_stdout(stdout_buf): + exec( # noqa: S102 + compile(code, "", "exec"), + {"__builtins__": self._SAFE_BUILTINS}, + ) + elapsed = (time.perf_counter() - start_time) * 1000 + output = stdout_buf.getvalue() or "(无输出)" + return ToolResult( + success=True, + output=f"执行成功 ({elapsed:.1f}ms):\n{output}", + metadata={"elapsed_ms": elapsed}, + ) + except Exception as exc: + return ToolResult(success=False, output=f"执行错误: {type(exc).__name__}: {exc}") \ No newline at end of file diff --git a/tools/file_reader.py b/tools/file_reader.py new file mode 100644 index 0000000..7942316 --- /dev/null +++ b/tools/file_reader.py @@ -0,0 +1,65 @@ +"""文件读取工具""" + + +# ════════════════════════════════════════════════════════════════ +# tools/file_reader.py — 文件读取工具 +# ════════════════════════════════════════════════════════════════ +""" +tools/file_reader.py +本地文件读取工具,支持文本文件,限制读取路径防止越权 +""" + +from pathlib import Path +from tools.base_tool import BaseTool, ToolResult + + +# 允许读取的根目录(沙箱限制) +_ALLOWED_ROOT = Path("./workspace") + + +class FileReaderTool(BaseTool): + name = "file_reader" + description = "读取本地文件内容,仅限 workspace/ 目录下的文件" + parameters = { + "path": { + "type": "string", + "description": "文件路径,相对于 workspace/ 目录", + }, + "encoding": { + "type": "string", + "description": "文件编码,默认 utf-8", + }, + } + + def execute(self, path: str, encoding: str = "utf-8", **_) -> ToolResult: + _ALLOWED_ROOT.mkdir(exist_ok=True) + + # 路径安全检查:防止目录穿越攻击 + target = (_ALLOWED_ROOT / path).resolve() + if not str(target).startswith(str(_ALLOWED_ROOT.resolve())): + return ToolResult(success=False, output=f"❌ 拒绝访问: 路径超出允许范围") + + if not target.exists(): + # Demo 模式:自动创建示例文件 + self._create_demo_file(target) + + try: + content = target.read_text(encoding=encoding) + return ToolResult( + success=True, + output=f"文件 [{path}] 内容:\n{content}", + metadata={"path": str(target), "size": target.stat().st_size}, + ) + except OSError as exc: + return ToolResult(success=False, output=f"读取失败: {exc}") + + @staticmethod + def _create_demo_file(path: Path) -> None: + """自动创建演示用文件""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + '{\n "app": "AgentDemo",\n "version": "1.0.0",\n' + ' "llm": "claude-sonnet-4-6",\n "tools": ["calculator", "web_search"]\n}\n', + encoding="utf-8", + ) + diff --git a/tools/web_search.py b/tools/web_search.py new file mode 100644 index 0000000..3b2a318 --- /dev/null +++ b/tools/web_search.py @@ -0,0 +1,65 @@ +"""网络搜索工具""" + +# ════════════════════════════════════════════════════════════════ +# tools/web_search.py — 网络搜索工具(模拟) +# ════════════════════════════════════════════════════════════════ +""" +tools/web_search.py +网络搜索工具(Demo 中使用模拟数据,生产环境可替换为真实 API) +""" + +import time +from tools.base_tool import BaseTool, ToolResult + + +# 模拟搜索结果数据库 +_MOCK_RESULTS: dict[str, list[dict]] = { + "天气": [ + {"title": "今日天气预报", "snippet": "晴转多云,气温 15°C ~ 24°C,东南风 3 级"}, + {"title": "未来 7 天天气", "snippet": "本周整体晴好,周末有小雨"}, + ], + "python": [ + {"title": "Python 官方文档", "snippet": "Python 3.12 新特性:改进的错误提示、更快的启动速度"}, + {"title": "Python 教程", "snippet": "从零开始学 Python,包含 300+ 实战案例"}, + ], +} +_DEFAULT_RESULTS = [ + {"title": "搜索结果 1", "snippet": "找到相关内容,请查看详情"}, + {"title": "搜索结果 2", "snippet": "更多相关信息可通过链接访问"}, +] + + +class WebSearchTool(BaseTool): + name = "web_search" + description = "在互联网上搜索信息,返回相关网页摘要" + parameters = { + "query": { + "type": "string", + "description": "搜索关键词或问题", + }, + "max_results": { + "type": "integer", + "description": "返回结果数量,默认 3", + }, + } + + def execute(self, query: str, max_results: int = 3, **_) -> ToolResult: + time.sleep(0.1) # 模拟网络延迟 + + # 关键词匹配模拟结果 + results = _DEFAULT_RESULTS + for keyword, data in _MOCK_RESULTS.items(): + if keyword in query: + results = data + break + + results = results[:max_results] + formatted = "\n".join( + f"[{i+1}] {r['title']}\n {r['snippet']}" + for i, r in enumerate(results) + ) + return ToolResult( + success=True, + output=f"搜索「{query}」,共 {len(results)} 条结果:\n{formatted}", + metadata={"query": query, "count": len(results)}, + ) diff --git a/utils/__pycache__/logger.cpython-310.pyc b/utils/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd83d7705844023a61ebb88a74591c11c82fb8c0 GIT binary patch literal 2424 zcmZWr|8EpU6yMpM{d#w8DPM#%(MwGur=dX*)W#6Xag-+agh4~eZ(x3djM(Ug0wv@1!d2eRkmzjC16LYKQy~fxGTgvTm8eLX zkf=(v1&L~xWun1Irv{7+2i5h<3K86_AZj`mwVW7@!8l&gXq+Y%RFa_bl$7f0z@DTh zH}3TQy3xCTr+4N4GK{5s3UPy$e8;af9q8i|CI4($NH%{628W?}dx1C-Y*-STB?^@Z zrV3FhCK^?VPBmgsotV@h7BxwXTCjACt7*64vN=3-dT&$Ex0`e7(CPONMr>ci_D5_@ zb|4zP6S2L`d@9Zj+isPbj%{2fgbpey@RD+Ard0%)j$6VGd`M$$a8rRaaq`~T9oHyU+rp( zQdg&F5x_*{EAlRBUI7iGYf=T~@GhxqElTqkY-3=H*DO`w@mr7&fdW($v%vL0y(K|E z5lS#MZsAQb`IZ!*Zr`F5>VW?3fq5+u5is*}v616rBe}7WsnL8a|?Xu9M1!!!5v}GX;j!NLJ$kr zv&$lzh#`F2un0HHZnN#S0G7lO-AI9JZRS*J=bwX`4T2M{Rl@7u1AXsJAcJTJ+NKPk z9g2+PRY2@UlV$mt1`W&j8A#N*?JvS*H1d>B?O-Z6Vwxc0^0_W@qSmT%^=RhE$%!aF z;(#1a=8qMaKs?b;jTiHi`3WW}6Hzn8Vxhv1EY^8338jEWyfW0d0sTs(2 zpI9qpbvSIxO2YT_tNXpLzj<=?^WMF0mmVxUe(>AUw+l%K9F(hM@80*1|NQdN#YBer#HM`O3AWhxdAS9=^2M(Epqk7T{5W0b|~@ zc)y(UHP5%*b8}`S0>PZx@hf}Y3#F~HO96{>`%jPTE_vnfrSr}tVI=Z01>KR|Q6h)K zF&H-2vd=q~>SrQJhzU6vMUEIAbVcHG<7}x#8}LYw#FWpRQuBB;=8AX0zR5M#tDQM#tf6mZYg<~dqpBzCOUaMqoe8%D%zGq+u;!aJSPLV&VSVWq)I;Vwyk z*=>0)yoPK0fx_fgZQ(Fp0f+0;oAM*91ftH^k&8p%&PxIrh*?NeFp4X2WFZ6SAiQD* zR0T^B^Xy$vO7(Hwwy9gT?JzoQA52E2PjAB?h(_(B@K{ma9}q&3hU_gtMB(8|4T_Z( n1vGTIX>VZ`hZ{6Lh+g?a;tz-?rm!R str: + level_color = self.LEVEL_COLORS.get(record.levelno, Color.RESET) + time_str = datetime.now().strftime("%H:%M:%S.%f")[:-3] + + # 从 logger 名称提取组件标签,例如 "agent.CLIENT" + component = record.name.split(".")[-1].upper() + comp_color = self.COMPONENT_COLORS.get(component, Color.RESET) + + prefix = ( + f"{Color.GREY}[{time_str}]{Color.RESET} " + f"{comp_color}{Color.BOLD}[{component:6s}]{Color.RESET} " + f"{level_color}{record.getMessage()}{Color.RESET}" + ) + return prefix + + +# ── Logger 工厂函数 ──────────────────────────────────────────── +def get_logger(component: str, level: int = logging.DEBUG) -> logging.Logger: + """ + 获取指定组件的 Logger 实例。 + + Args: + component: 组件名称,如 "CLIENT"、"LLM"、"MCP" + level: 日志级别,默认 DEBUG + + Returns: + 配置好的 Logger 实例 + """ + logger = logging.getLogger(f"agent.{component}") + logger.setLevel(level) + + # 避免重复添加 Handler + if logger.handlers: + return logger + + # 终端 Handler(彩色) + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(ColorFormatter()) + logger.addHandler(console_handler) + + # 文件 Handler(纯文本) + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + file_handler = logging.FileHandler(log_dir / "agent.log", encoding="utf-8") + file_handler.setFormatter( + logging.Formatter("[%(asctime)s] [%(name)s] %(levelname)s: %(message)s") + ) + logger.addHandler(file_handler) + + # 防止日志向上传播到 root logger + logger.propagate = False + return logger \ No newline at end of file