commit 1a09129395bd11f28a48751836cd7fa5dbac3b1c Author: liusongtao Date: Sat Feb 28 16:21:35 2026 +0800 first agent project 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 0000000..226faba Binary files /dev/null and b/client/__pycache__/__init__.cpython-310.pyc differ diff --git a/client/__pycache__/agent_client.cpython-310.pyc b/client/__pycache__/agent_client.cpython-310.pyc new file mode 100644 index 0000000..5ab36a2 Binary files /dev/null and b/client/__pycache__/agent_client.cpython-310.pyc differ 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 0000000..232cd1d Binary files /dev/null and b/llm/__pycache__/__init__.cpython-310.pyc differ diff --git a/llm/__pycache__/llm_engine.cpython-310.pyc b/llm/__pycache__/llm_engine.cpython-310.pyc new file mode 100644 index 0000000..b5889d6 Binary files /dev/null and b/llm/__pycache__/llm_engine.cpython-310.pyc differ 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 0000000..df2f6cc Binary files /dev/null and b/mcp/__pycache__/__init__.cpython-310.pyc differ diff --git a/mcp/__pycache__/mcp_protocol.cpython-310.pyc b/mcp/__pycache__/mcp_protocol.cpython-310.pyc new file mode 100644 index 0000000..1cb948f Binary files /dev/null and b/mcp/__pycache__/mcp_protocol.cpython-310.pyc differ diff --git a/mcp/__pycache__/mcp_server.cpython-310.pyc b/mcp/__pycache__/mcp_server.cpython-310.pyc new file mode 100644 index 0000000..4f95d1e Binary files /dev/null and b/mcp/__pycache__/mcp_server.cpython-310.pyc differ diff --git a/mcp/mcp_protocol.py b/mcp/mcp_protocol.py new file mode 100644 index 0000000..2126da5 --- /dev/null +++ b/mcp/mcp_protocol.py @@ -0,0 +1,107 @@ +"""MCP 协议:JSON-RPC 消息定义""" +""" +mcp/mcp_protocol.py +MCP (Model Context Protocol) 协议数据结构定义 +基于 JSON-RPC 2.0 规范封装请求/响应消息体 +""" + +import uuid +from dataclasses import dataclass, field +from typing import Any + + +# ── MCP 方法常量 ─────────────────────────────────────────────── +class MCPMethod: + TOOLS_LIST = "tools/list" # 列出所有可用工具 + TOOLS_CALL = "tools/call" # 调用指定工具 + RESOURCES_READ = "resources/read" # 读取资源 + + +# ── 请求消息 ─────────────────────────────────────────────────── +@dataclass +class MCPRequest: + """ + MCP 工具调用请求(JSON-RPC 2.0 格式) + + 示例: + { + "jsonrpc": "2.0", + "id": "abc-123", + "method": "tools/call", + "params": { + "name": "calculator", + "arguments": {"expression": "1+1"} + } + } + """ + method: str + params: dict[str, Any] = field(default_factory=dict) + jsonrpc: str = "2.0" + id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) + + def to_dict(self) -> 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 0000000..099cc76 Binary files /dev/null and b/memory/__pycache__/__init__.cpython-310.pyc differ diff --git a/memory/__pycache__/memory_store.cpython-310.pyc b/memory/__pycache__/memory_store.cpython-310.pyc new file mode 100644 index 0000000..79975c8 Binary files /dev/null and b/memory/__pycache__/memory_store.cpython-310.pyc differ 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 0000000..f8aa8fe Binary files /dev/null and b/tools/__pycache__/__init__.cpython-310.pyc differ diff --git a/tools/__pycache__/base_tool.cpython-310.pyc b/tools/__pycache__/base_tool.cpython-310.pyc new file mode 100644 index 0000000..4b303e2 Binary files /dev/null and b/tools/__pycache__/base_tool.cpython-310.pyc differ diff --git a/tools/__pycache__/calculator.cpython-310.pyc b/tools/__pycache__/calculator.cpython-310.pyc new file mode 100644 index 0000000..d7b5b4f Binary files /dev/null and b/tools/__pycache__/calculator.cpython-310.pyc differ diff --git a/tools/__pycache__/code_executor.cpython-310.pyc b/tools/__pycache__/code_executor.cpython-310.pyc new file mode 100644 index 0000000..c69976c Binary files /dev/null and b/tools/__pycache__/code_executor.cpython-310.pyc differ diff --git a/tools/__pycache__/file_reader.cpython-310.pyc b/tools/__pycache__/file_reader.cpython-310.pyc new file mode 100644 index 0000000..2f8fd37 Binary files /dev/null and b/tools/__pycache__/file_reader.cpython-310.pyc differ diff --git a/tools/__pycache__/web_search.cpython-310.pyc b/tools/__pycache__/web_search.cpython-310.pyc new file mode 100644 index 0000000..81f3f95 Binary files /dev/null and b/tools/__pycache__/web_search.cpython-310.pyc differ 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 0000000..bd83d77 Binary files /dev/null and b/utils/__pycache__/logger.cpython-310.pyc differ diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..90191a6 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,96 @@ +"""日志工具""" +""" +utils/logger.py +统一日志模块,支持彩色终端输出与文件记录 +""" + +import logging +import sys +from datetime import datetime +from pathlib import Path + + +# ── ANSI 颜色常量 ────────────────────────────────────────────── +class Color: + RESET = "\033[0m" + BOLD = "\033[1m" + CYAN = "\033[96m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + MAGENTA = "\033[95m" + BLUE = "\033[94m" + GREY = "\033[90m" + + +# ── 自定义彩色 Formatter ─────────────────────────────────────── +class ColorFormatter(logging.Formatter): + LEVEL_COLORS = { + logging.DEBUG: Color.GREY, + logging.INFO: Color.CYAN, + logging.WARNING: Color.YELLOW, + logging.ERROR: Color.RED, + logging.CRITICAL: Color.MAGENTA, + } + + COMPONENT_COLORS = { + "CLIENT": Color.BLUE, + "LLM": Color.GREEN, + "MCP": Color.YELLOW, + "TOOL": Color.MAGENTA, + "MEMORY": Color.CYAN, + "SYSTEM": Color.GREY, + } + + def format(self, record: logging.LogRecord) -> 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