| """
|
| Hybrid ReAct Agent - 企业级架构实现
|
| 结合 ReAct 的思维链 (CoT) 与 Function Calling 的结构化执行
|
| + 四层自动记忆 + 长期记忆自动注入(真正个性化)
|
| """
|
|
|
| import asyncio
|
| import json
|
| import logging
|
| from typing import Optional, List, Dict, Any
|
|
|
| from helloAgents.tools.builtin.memory_tool import MemoryTool
|
| from ..core.agent import Agent
|
| from ..core.llm import HelloAgentsLLM
|
| from ..core.config import Config
|
| from ..tools.registry import ToolRegistry
|
| from redis_config import get_redis, QUEUE_MEMORY
|
| redis = get_redis()
|
|
|
|
|
| DEFAULT_HYBRID_REACT_PROMPT = """你是一个具备深度推理和行动能力的企业级 AI 助手。
|
|
|
| ## 可用工具
|
| {tools}
|
|
|
| ## 工作流程(严格遵守)
|
| 1. **思考**:在 content 中自然写出你的分析、推理、判断。
|
| - 不需要任何前缀,不要写 Thought、Action、Finish。
|
|
|
| 2. **行动**:需要外部信息时,使用 tool_calls 调用工具。
|
| - 不要在文本里模拟工具调用。
|
|
|
| 3. **结束**:信息足够回答用户时,
|
| - 直接输出最终答案
|
| - 不输出 tool_calls
|
| - 不再写思考过程
|
|
|
| ## 重要规则
|
| - 每次必须先思考再决定是否调用工具。
|
| - 无 tool_calls 代表直接回答。
|
| - 最终回答必须清晰、直接、干净。
|
|
|
| ## 用户的记忆
|
| {memory}
|
|
|
| ## 当前任务
|
| **Question:** {question}
|
|
|
| ## 执行历史
|
| {history}
|
|
|
| 请根据以上信息进行推理并回应:"""
|
|
|
|
|
| class ReActAgent(Agent):
|
| """
|
| 企业级 Hybrid ReAct Agent + 四层自动记忆 + 长期记忆自动注入
|
| 架构:记忆完全基座化,对 LLM 透明,业务工具无耦合
|
| """
|
|
|
| def __init__(
|
| self,
|
| name: str,
|
| llm: HelloAgentsLLM,
|
| tool_registry: Optional[ToolRegistry] = None,
|
| system_prompt: Optional[str] = None,
|
| config: Optional[Config] = None,
|
| max_steps: int = 5,
|
| custom_prompt: Optional[str] = None
|
| ):
|
| super().__init__(name, llm, system_prompt, config)
|
|
|
| self.tool_registry = tool_registry if tool_registry else ToolRegistry()
|
| self.max_steps = max_steps
|
| self.prompt_template = custom_prompt or DEFAULT_HYBRID_REACT_PROMPT
|
| self._tool_schemas = self._build_tool_schemas()
|
|
|
| def _build_tool_schemas(self) -> List[Dict[str, Any]]:
|
| """构建 OpenAI 兼容的工具 Schema(memory 工具对 LLM 隐藏)"""
|
| if not self.tool_registry:
|
| return []
|
|
|
| schemas = []
|
| for tool in self.tool_registry.get_all_tools():
|
| if tool.name == "memory":
|
| continue
|
|
|
| properties = {}
|
| required = []
|
| try:
|
| params = tool.get_parameters()
|
| for p in params:
|
| p_type = (p.type or "string").lower()
|
| if p_type not in ["string", "number", "integer", "boolean", "array", "object"]:
|
| p_type = "string"
|
|
|
| properties[p.name] = {
|
| "type": p_type,
|
| "description": p.description or ""
|
| }
|
| if getattr(p, "required", True):
|
| required.append(p.name)
|
| except Exception:
|
| properties["input"] = {"type": "string", "description": "输入内容"}
|
| required = ["input"]
|
|
|
| schemas.append({
|
| "type": "function",
|
| "function": {
|
| "name": tool.name,
|
| "description": tool.description or "",
|
| "parameters": {
|
| "type": "object",
|
| "properties": properties,
|
| "required": required
|
| }
|
| }
|
| })
|
|
|
|
|
| func_map = getattr(self.tool_registry, "_functions", {})
|
| for name, info in func_map.items():
|
| if not any(s["function"]["name"] == name for s in schemas):
|
| schemas.append({
|
| "type": "function",
|
| "function": {
|
| "name": name,
|
| "description": info.get("description", ""),
|
| "parameters": {
|
| "type": "object",
|
| "properties": {"input": {"type": "string", "description": "输入文本"}},
|
| "required": ["input"]
|
| }
|
| }
|
| })
|
| return schemas
|
|
|
| def add_tool(self, tool):
|
| self.tool_registry.register_tool(tool)
|
| self._tool_schemas = self._build_tool_schemas()
|
|
|
| def _convert_params(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
| tool = self.tool_registry.get_tool(tool_name)
|
| if not tool:
|
| return args
|
|
|
| try:
|
| params = tool.get_parameters()
|
| type_map = {p.name: p.type.lower() for p in params}
|
| converted = {}
|
| for k, v in args.items():
|
| t = type_map.get(k, "string")
|
| try:
|
| if t in ["integer", "int"]:
|
| converted[k] = int(v)
|
| elif t in ["number", "float"]:
|
| converted[k] = float(v)
|
| elif t in ["boolean", "bool"]:
|
| converted[k] = str(v).lower() in ["true", "1", "yes"]
|
| else:
|
| converted[k] = v
|
| except (ValueError, TypeError):
|
| converted[k] = v
|
| return converted
|
| except Exception:
|
| return args
|
|
|
|
|
|
|
|
|
| async def _get_user_long_term_memory(self, input_text: str, **kwargs) -> str:
|
| try:
|
| memory_tool: MemoryTool = self.tool_registry.get_tool("memory")
|
| if not memory_tool:
|
| return "无用户记忆"
|
|
|
| task = {
|
| "tool_name": "memory",
|
| "input_data": {
|
| "action": "search",
|
| "query": input_text,
|
| "memory_types": ["working", "semantic", "episodic"],
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "limit": 8
|
| }
|
| }
|
|
|
| from ..tools.async_executor import run_parallel_tools
|
| results = await run_parallel_tools(self.tool_registry, [task])
|
| memory_result = results[0]["result"] if results else ""
|
| return f"\n{memory_result}" if memory_result else "无用户记忆"
|
| except Exception:
|
| return "无用户记忆"
|
|
|
|
|
|
|
|
|
| def _auto_save_all_memories(self, user_input: str, final_answer: str, system_prompt: str, **kwargs):
|
|
|
| work_tasks = {
|
| "action": "add",
|
| "user_content": user_input,
|
| "assistant_content": final_answer,
|
| "memory_type": "working",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "importance": 0.3
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(work_tasks))
|
|
|
|
|
| semantic_tasks = {
|
| "action": "add",
|
| "content": user_input,
|
| "memory_type": "semantic",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": None,
|
| "importance": 0.8
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(semantic_tasks))
|
|
|
|
|
| episodic_tasks = {
|
| "action": "add",
|
| "role": "user",
|
| "content": user_input,
|
| "memory_type": "episodic",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "importance": 0.5
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(episodic_tasks))
|
|
|
| episodic_answer_tasks = {
|
| "action": "add",
|
| "role": "assistant",
|
| "content": final_answer,
|
| "memory_type": "episodic",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "importance": 0.5
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(episodic_answer_tasks))
|
|
|
| episodic_observation_tasks = {
|
| "action": "add",
|
| "role": "observation",
|
| "content": system_prompt,
|
| "memory_type": "episodic",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "importance": 0.5
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(episodic_observation_tasks))
|
|
|
| summary = self._extract_summary_by_llm(user_input, final_answer, **kwargs)
|
| if summary:
|
| summary_tasks = {
|
| "action": "add",
|
| "role": "summary",
|
| "content": summary,
|
| "memory_type": "episodic",
|
| "user_id": kwargs.get("user_id"),
|
| "session_id": kwargs.get("session_id"),
|
| "importance": 0.5
|
| }
|
| redis.lpush(QUEUE_MEMORY, json.dumps(summary_tasks))
|
|
|
| def _extract_summary_by_llm(self, user_input: str, final_answer: str, **kwargs) -> str:
|
| """
|
| 让 LLM 根据用户问题 + 助手回复 提取核心摘要,用于精简记忆存储
|
| """
|
| try:
|
|
|
| prompt = f"""
|
| 请你对以下【用户问题】和【助手回复】进行精简摘要提取:
|
| 要求:
|
| 1. 只保留核心信息,不超过50字
|
| 2. 语言简洁、客观、通顺
|
| 3. 直接输出摘要,不要额外解释
|
|
|
| 用户问题:{user_input}
|
| 助手回复:{final_answer}
|
| 摘要:
|
| """
|
|
|
|
|
|
|
| messages = [{"role": "user", "content": prompt}]
|
| summary = self.llm.invoke(messages)
|
|
|
|
|
| summary = summary.strip()
|
| return summary if summary else ""
|
|
|
| except Exception as e:
|
| print(f"LLM 提取摘要失败:{str(e)}")
|
| return ""
|
|
|
|
|
|
|
|
|
| async def run(self, input_text: str, **kwargs) -> str:
|
| current_step = 0
|
| final_answer = ""
|
| react_trace = []
|
| system_prompt = ""
|
| print(f"\n🤖 {self.name} 开始处理:{input_text}")
|
| memory = await self._get_user_long_term_memory(input_text, **kwargs)
|
| tools_desc = self.tool_registry.get_tools_description()
|
| while current_step < self.max_steps:
|
| current_step += 1
|
| print(f"\n--- 第 {current_step} 步 ---")
|
|
|
|
|
| history_str = self._build_react_history_str(react_trace)
|
| system_prompt = self.prompt_template.format(
|
| tools=tools_desc,
|
| question=input_text,
|
| history=history_str,
|
| memory=memory
|
| )
|
|
|
|
|
| llm_messages = [{"role": "user", "content": system_prompt}]
|
|
|
| try:
|
| client = self.llm._client
|
| response = client.chat.completions.create(
|
| model=self.llm.model,
|
| messages=llm_messages,
|
| tools=self._tool_schemas or None,
|
| tool_choice="auto" if self._tool_schemas else None,
|
| temperature=self.llm.temperature,
|
| max_tokens=self.llm.max_tokens
|
| )
|
| assistant_message = response.choices[0].message
|
| content = assistant_message.content or ""
|
| tool_calls = assistant_message.tool_calls or []
|
| except Exception as e:
|
| print(f"❌ LLM 调用失败:{e}")
|
| assistant_entry = {
|
| "role": "assistant",
|
| "content": f"LLM 调用失败:{str(e)}"
|
| }
|
| react_trace.append(assistant_entry)
|
| history_str = self._build_react_history_str(react_trace)
|
| system_prompt = self.prompt_template.format(
|
| tools=tools_desc,
|
| question=input_text,
|
| history=history_str,
|
| memory=memory
|
| )
|
| break
|
|
|
|
|
|
|
| assistant_entry = {
|
| "role": "assistant",
|
| "content": content
|
| }
|
|
|
|
|
| if tool_calls:
|
| assistant_entry["tool_calls"] = [
|
| {
|
| "id": tc.id,
|
| "function": {
|
| "name": tc.function.name,
|
| "arguments": tc.function.arguments
|
| }
|
| } for tc in tool_calls
|
| ]
|
|
|
| react_trace.append(assistant_entry)
|
|
|
|
|
| if not tool_calls:
|
| final_answer = content
|
| history_str = self._build_react_history_str(react_trace)
|
| system_prompt = self.prompt_template.format(
|
| tools=tools_desc,
|
| question=input_text,
|
| history=history_str,
|
| memory=memory
|
| )
|
| break
|
|
|
|
|
| parallel_tasks = []
|
| tool_map = {}
|
| for idx, tc in enumerate(tool_calls):
|
| func_name = tc.function.name
|
| try:
|
| args = json.loads(tc.function.arguments)
|
| except json.JSONDecodeError:
|
| args = {}
|
| if func_name != "send_qq_email":
|
| args["user_id"] = kwargs.get("user_id")
|
| args["session_id"] = kwargs.get("session_id")
|
| args = self._convert_params(func_name, args)
|
|
|
| parallel_tasks.append({"tool_name": func_name, "input_data": args})
|
| tool_map[idx] = tc
|
|
|
| from ..tools.async_executor import run_parallel_tools
|
| results = await run_parallel_tools(self.tool_registry, parallel_tasks, max_workers=4)
|
|
|
|
|
| for idx, res in enumerate(results):
|
| tc = tool_map[idx]
|
| try:
|
| args = json.loads(tc.function.arguments)
|
| except:
|
| args = {}
|
| react_trace.append({
|
| "role": "tool",
|
| "tool_call_id": tc.id,
|
| "name": res["tool_name"],
|
| "arguments": tc.function.arguments,
|
| "content": str(res["result"])
|
| })
|
|
|
| if not final_answer:
|
| final_answer = "抱歉,无法在限定步数内完成任务。"
|
| assistant_entry = {
|
| "role": "assistant",
|
| "content": final_answer
|
| }
|
| react_trace.append(assistant_entry)
|
| history_str = self._build_react_history_str(react_trace)
|
| system_prompt = self.prompt_template.format(
|
| tools=tools_desc,
|
| question=input_text,
|
| history=history_str,
|
| memory=memory
|
| )
|
|
|
|
|
| try:
|
| self._auto_save_all_memories(input_text, final_answer, system_prompt, **kwargs)
|
| except Exception:
|
| pass
|
|
|
| return final_answer
|
|
|
|
|
|
|
|
|
| def _build_react_history_str(self, react_trace: List[Dict[str, Any]]) -> str:
|
| lines = []
|
| step = 1
|
| for msg in react_trace:
|
| content = msg.get("content", "").strip()
|
|
|
| if msg["role"] == "assistant":
|
|
|
| if content:
|
| lines.append(f"Step {step} - Thought: {content}")
|
| step += 1
|
|
|
|
|
| tool_calls = msg.get("tool_calls", [])
|
| for tc in tool_calls:
|
| func = tc.get("function", {})
|
| func_name = func.get("name", "unknown_tool")
|
| arguments = func.get("arguments", "{}")
|
|
|
| lines.append(f"Step {step} - Action: 调用工具 [{func_name}],参数:{arguments}")
|
| step += 1
|
|
|
| elif msg["role"] == "tool":
|
|
|
| tool_name = msg.get("name", "unknown_tool")
|
| arguments = msg.get("arguments", "{}")
|
| if content:
|
| lines.append(f"Step {step} - Observation: 工具 [{tool_name}] 参数:{arguments} 返回:{content}")
|
| step += 1
|
|
|
| return "\n".join(lines) if lines else "无历史记录" |