Compare commits
No commits in common. "master" and "main" have entirely different histories.
|
|
@ -1 +0,0 @@
|
|||
"""__init__.py"""
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
"""客户端:用户交互 & 会话管理"""
|
||||
"""
|
||||
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,
|
||||
prompt: str = ""
|
||||
):
|
||||
self.llm = llm
|
||||
self.mcp_server = mcp_server
|
||||
self.memory = memory
|
||||
self.agent_prompt = prompt
|
||||
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, self.agent_prompt)
|
||||
|
||||
# ── 分支:是否需要工具 ──────────────────────────────────
|
||||
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("🗑 会话已清空")
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""__init__.py"""
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
"""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
|
||||
import os
|
||||
|
||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
# ── 工具调用决策结果 ───────────────────────────────────────────
|
||||
@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)
|
||||
self.logger = get_logger("Monica")
|
||||
|
||||
def create(self, model_name: str, tool_schemas, user_input: str, agent_prompt: str = "") -> ToolDecision:
|
||||
tools = [{
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"parameters": s.parameters} for s in tool_schemas]
|
||||
messages = []
|
||||
if agent_prompt:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": agent_prompt,
|
||||
})
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": user_input
|
||||
}]
|
||||
})
|
||||
completion = self.client.chat.completions.create(
|
||||
model=model_name,
|
||||
functions=tools,
|
||||
messages=messages
|
||||
)
|
||||
self.logger.info(completion.choices[0].message.content)
|
||||
response = json.loads(completion.choices[0].message.content)
|
||||
return ToolDecision(need_tool=response['need_tool'],
|
||||
tool_name=response['tool_name'],
|
||||
arguments=response['arguments'],
|
||||
reasoning=response['reasoning'])
|
||||
|
||||
def chat(self, model_name: str, user_input: str, context: str = '') -> str:
|
||||
message = f"""##用户输入\n{user_input}\n\n"""\
|
||||
f"""##历史消息\n{context}\n\n"""
|
||||
|
||||
messages = [{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": message
|
||||
}]
|
||||
}]
|
||||
completion = self.client.chat.completions.create(
|
||||
model=model_name,
|
||||
messages=messages
|
||||
)
|
||||
self.logger.info(completion.choices[0].message.content)
|
||||
return completion.choices[0].message.content
|
||||
|
||||
|
||||
# ── LLM 引擎 ──────────────────────────────────────────────────
|
||||
class LLMEngine:
|
||||
"""
|
||||
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 = "",
|
||||
agent_prompt: str = ""
|
||||
) -> ToolDecision:
|
||||
"""
|
||||
Step 1 & 2: 理解意图,决策工具调用(Think 阶段)
|
||||
|
||||
Args:
|
||||
user_input: 用户输入文本
|
||||
tool_schemas: 可用工具的 Schema 列表
|
||||
context: 对话历史上下文摘要
|
||||
agent_prompt: 智能体提示词
|
||||
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, agent_prompt=agent_prompt)
|
||||
|
||||
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 self.client.chat(self.model_name, user_input, context=context)
|
||||
|
||||
# ── 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], agent_prompt: str = "") -> 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
|
||||
"""
|
||||
if self.client:
|
||||
return self.client.create(self.model_name,
|
||||
user_input=user_input,
|
||||
tool_schemas=tool_schemas,
|
||||
agent_prompt=agent_prompt)
|
||||
else:
|
||||
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}*"
|
||||
)
|
||||
1164
logs/agent.log
1164
logs/agent.log
File diff suppressed because it is too large
Load Diff
164
main.py
164
main.py
|
|
@ -1,164 +0,0 @@
|
|||
"""程序入口"""
|
||||
"""
|
||||
main.py
|
||||
智能体 Demo 程序入口
|
||||
组装所有模块,启动交互式对话循环
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
# ── 导入各模块 ─────────────────────────────────────────────────
|
||||
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 tools.tool_generator import ToolGeneratorTool
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger("SYSTEM")
|
||||
|
||||
|
||||
# ── 系统组装 ───────────────────────────────────────────────────
|
||||
def build_agent(agent_prompt) -> 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,
|
||||
ToolGeneratorTool
|
||||
)
|
||||
|
||||
# 2. LLM 引擎
|
||||
llm = LLMEngine(model_name="gpt-4o")
|
||||
|
||||
# 3. 记忆模块
|
||||
memory = MemoryStore(max_history=20)
|
||||
|
||||
# 4. 组装客户端
|
||||
client = AgentClient(llm=llm, mcp_server=mcp_server, memory=memory, prompt=agent_prompt)
|
||||
|
||||
logger.info(f"✅ Agent 组装完成,已注册工具: {mcp_server.list_tools()}")
|
||||
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 → 演示模式(自动执行预设场景)
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-d", "--daemon", help="服务模式", action="store_true")
|
||||
parser.add_argument("-p", "--prompt", default="你是一个通用智能体,非常擅长将用户指令分解成可以执行的任务进行执行。", help="智能体提示此词, 例如:你是一个XXXXX,非常擅长……")
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
client = build_agent(args.prompt)
|
||||
|
||||
if args.daemon:
|
||||
run_demo(client)
|
||||
else:
|
||||
run_interactive(client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""__init__.py"""
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
"""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,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
"""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()})"
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""__init__.py"""
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
"""记忆模块:对话历史管理"""
|
||||
"""
|
||||
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]}...")
|
||||
|
|
@ -1 +0,0 @@
|
|||
openai
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""__init__.py"""
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"""
|
||||
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}")
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
"""计算器工具"""
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 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__}")
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"""代码执行工具"""
|
||||
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 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, "<agent_sandbox>", "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}")
|
||||
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""文件读取工具"""
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 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",
|
||||
)
|
||||
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import json
|
||||
|
||||
|
||||
|
||||
|
||||
class MyDemoTool(BaseTool):
|
||||
name = "{{ name }}"
|
||||
description = "{{ description }}"
|
||||
parameters = {{ parameters }}
|
||||
def execute(self, parameter1: str, parameter2: int):
|
||||
...
|
||||
"""
|
||||
|
||||
"""
|
||||
生成agent技能工具用于实现当前系统进程状态查询,技能工具代码示例如下:"
|
||||
"""
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
from tools.base_tool import BaseTool, ToolResult
|
||||
|
||||
|
||||
class ToolGeneratorTool(BaseTool):
|
||||
name = "tool_generator"
|
||||
description = "生成agent工具代码"
|
||||
parameters = {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "tool name",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "tool description"
|
||||
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"description": """tool parameters descriptions"""
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "the code that the LLM generated"
|
||||
}
|
||||
}
|
||||
|
||||
def execute(self, name: str, description: str, parameters: dict, code: str) -> ToolResult:
|
||||
pass
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""网络搜索工具"""
|
||||
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 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)},
|
||||
)
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
"""日志工具"""
|
||||
"""
|
||||
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
|
||||
Loading…
Reference in New Issue