import json import uuid from typing import Generator, List, Dict, Any, Union import openai import public from public import lang from mod.project.agent.chat_client.memory import MemoryManager from mod.project.agent.chat_client.tools import registry class SimpleAgent: def __init__(self, session_id: str, config: Dict[str, Any] = None): self.session_id = session_id self.config = config or {} # 提取配置 self.api_key = self.config.get("api_key") base_url = self.config.get("base_url") self.base_url = base_url self.model_name = self.config.get("model_name") # 简单Agent通常不需要复杂的工具迭代,但为了兼容性保留基本配置读取,虽然不使用工具 self.default_headers = self.config.get("default_headers", {}) self.system_prompt = self.config.get("system_prompt", "") self.temperature = self.config.get("temperature", 1) self.top_p = self.config.get("top_p", 1) self.enabled_tools = self.config.get("tools", []) default_non_code_tools = ["Skills"] for tool in default_non_code_tools: if tool not in self.enabled_tools: self.enabled_tools.append(tool) # 显式设置滑动窗口大小为极大值,以实现"全量记忆" # 用户提到通常在10轮内,设置100足够覆盖全量 self.memory = MemoryManager( session_id=session_id, sessions_dir=self.config.get("sessions_dir", "sessions"), sliding_window_size=self.config.get("sliding_window_size", 100), model_name=self.model_name ) self.client = openai.OpenAI( api_key=self.api_key, base_url=self.base_url, default_headers=self.default_headers ) def close(self): """ 关闭 Agent,释放资源。 """ self.client.close() def _get_model_provider(self, model_name: str) -> str: """ 根据模型名称识别厂商 返回: qwen, doubao, gpt, claude, gemini, deepseek, kimi, glm, hunyuan, ernie, other """ name_lower = model_name.lower() if any(x in name_lower for x in ["qwen", "qwq"]): return "qwen" elif "doubao" in name_lower or "seed" in name_lower: return "doubao" elif "gpt" in name_lower: return "gpt" elif "claude" in name_lower or "anthropic" in name_lower: return "claude" elif "gemini" in name_lower: return "gemini" elif "deepseek" in name_lower: return "deepseek" elif "kimi" in name_lower or "moonshot" in name_lower: return "kimi" elif "glm" in name_lower or "zhipu" in name_lower: return "glm" elif "hunyuan" in name_lower: return "hunyuan" elif "ernie" in name_lower: return "ernie" elif "grok" in name_lower: return "grok" else: return "other" def chat(self, user_input: Union[str, List[Dict[str, Any]]]) -> Generator[Dict[str, Any], None, None]: """ 简单Agent聊天循环,支持流式响应。 已添加工具调用支持。 """ try: # 生成 ID user_msg_id = str(uuid.uuid4()) ai_msg_id = str(uuid.uuid4()) # 处理文件引用 user_text = user_input if isinstance(user_input, list): text_parts = [] for item in user_input: if isinstance(item, dict) and item.get("type") == "text": text_parts.append(item.get("text", "")) user_text = "\n".join(text_parts) # 1. 更新记忆 (用户) user_msg = self.memory.add_message("user", user_input, id=user_msg_id) # RAG 检索 context_str = "" # 2. 构建消息 (使用全量记忆,无RAG) messages = self._build_messages(context_str) self.enabled_tools.append("get_panel_info") tools = registry.get_openai_tools(enabled_ids=self.enabled_tools) # 循环限制,防止无限递归 max_tool_iterations = self.config.get("max_tool_iterations", 10) iteration_count = 0 full_response_content = "" # 最终累积响应 full_reasoning_content = "" # 最终累积思考 tool_call_chunks = {} while iteration_count < max_tool_iterations: iteration_count += 1 # Reset last_loop_tokens for this iteration last_loop_tokens = { "total_tokens": 0, "input_tokens": 0, "output_tokens": 0 } # Claude 系模型不接受 temperature 与 top_p 同时指定 (Bedrock/Anthropic 校验 400), 二选一仅保留 temperature _is_claude = any(x in (self.model_name or "").lower() for x in ("claude", "anthropic")) response_stream = self.client.chat.completions.create( model=self.model_name, messages=messages, stream=True, stream_options={"include_usage": True}, temperature=self.temperature, tools=tools if tools else None, **({} if _is_claude else {"top_p": self.top_p}) ) # 工具调用累加器 tool_call_chunks = {} current_response_content = "" current_reasoning_content = "" for chunk in response_stream: # 结束判断 if not chunk.choices: if chunk.usage: last_loop_tokens["total_tokens"] = chunk.usage.total_tokens last_loop_tokens["input_tokens"] = chunk.usage.prompt_tokens last_loop_tokens["output_tokens"] = chunk.usage.completion_tokens yield { "type": "stop", "usage": { "total_tokens": chunk.usage.total_tokens, "input_tokens": chunk.usage.prompt_tokens, "output_tokens": chunk.usage.completion_tokens }, "message_id": chunk.id } continue delta = chunk.choices[0].delta # 处理推理内容 # 注意:只有部分模型(Qwen等)支持 reasoning_content # 其他模型(GPT、Claude、Gemini等)不返回此字段,需要容错处理 if hasattr(delta, "reasoning_content") and delta.reasoning_content: current_reasoning_content += delta.reasoning_content yield { "type": "reasoning", "response": delta.reasoning_content } # 处理正文内容 if delta.content: current_response_content += delta.content yield { "type": "content", "response": delta.content } # 处理工具调用 if delta.tool_calls: for tc in delta.tool_calls: index = tc.index if index not in tool_call_chunks: tool_call_chunks[index] = { "id": tc.id, "function": {"name": "", "arguments": ""} } if tc.id: tool_call_chunks[index]["id"] = tc.id if tc.function.name: tool_call_chunks[index]["function"]["name"] += tc.function.name if tc.function.arguments: tool_call_chunks[index]["function"]["arguments"] += tc.function.arguments # 如果这一轮有内容,累加到最终响应 if current_response_content: full_response_content = current_response_content if current_reasoning_content: full_reasoning_content = current_reasoning_content # 如果没有工具调用,结束循环 if not tool_call_chunks: break # 处理工具调用逻辑 assistant_msg_kwargs = {"tool_calls": []} for idx in sorted(tool_call_chunks.keys()): tc = tool_call_chunks[idx] assistant_msg_kwargs["tool_calls"].append({ "id": tc["id"], "type": "function", "function": tc["function"] }) # 保存助手工具调用消息 if current_reasoning_content: assistant_msg_kwargs["reasoning_content"] = current_reasoning_content self.memory.add_message("assistant", current_response_content, id=ai_msg_id, **assistant_msg_kwargs) messages.append({ "role": "assistant", "content": current_response_content, "tool_calls": assistant_msg_kwargs["tool_calls"] }) # --- 循环调用保护检测 --- is_duplicate_call = False if iteration_count > 1 and messages: last_assistant_msg = None for i in range(len(messages) - 2, -1, -1): if messages[i]["role"] == "assistant": last_assistant_msg = messages[i] break if last_assistant_msg and "tool_calls" in last_assistant_msg: current_calls_dump = json.dumps([{k: v for k, v in tc.items() if k != 'id'} for tc in assistant_msg_kwargs["tool_calls"]], sort_keys=True) prev_calls_dump = json.dumps([{k: v for k, v in tc.items() if k != 'id'} for tc in last_assistant_msg["tool_calls"]], sort_keys=True) if current_calls_dump == prev_calls_dump: is_duplicate_call = True if is_duplicate_call: for tc in assistant_msg_kwargs["tool_calls"]: func_name = tc["function"]["name"] call_id = tc["id"] err_msg = "System Monitor: Detected repeated tool execution with identical arguments. Please do not run the same tool again. Analyze the PREVIOUS results and provide your final answer immediately." yield { "type": "tool_result", "tool": func_name, "result": err_msg, "id": call_id } self.memory.add_message("tool", err_msg, tool_call_id=call_id, id=ai_msg_id) messages.append({ "role": "tool", "tool_call_id": call_id, "content": err_msg }) continue # ------------------------ # 执行工具 for tc in assistant_msg_kwargs["tool_calls"]: func_name = tc["function"]["name"] args_str = tc["function"]["arguments"] call_id = tc["id"] yield { "type": "tool_call", "tool": func_name, "args": args_str, "id": call_id } try: args = json.loads(args_str) func = registry.get_tool_func(func_name) if func: result = func(**args) # 工具返回字符串时直接使用,避免重复序列化 if isinstance(result, str): result_str = result else: result_str = json.dumps(result, ensure_ascii=False) else: result_str = f"Error: Tool {func_name} not found." except Exception as e: result_str = f"Error executing tool: {e}" yield { "type": "tool_result", "tool": func_name, "result": result_str[:1000] + '···', "id": call_id } self.memory.add_message("tool", result_str, tool_call_id=call_id, id=ai_msg_id) messages.append({ "role": "tool", "tool_call_id": call_id, "content": result_str }) # 4. 最终记忆更新 (助手响应) if not tool_call_chunks and full_response_content: kwargs = {} if full_reasoning_content: kwargs["reasoning_content"] = full_reasoning_content self.memory.add_message("assistant", full_response_content, id=ai_msg_id, **kwargs) # 更新 meta.json 中的 token 使用量 self.memory.update_meta_tokens( total_tokens=last_loop_tokens["total_tokens"], input_tokens=last_loop_tokens["input_tokens"], output_tokens=last_loop_tokens["output_tokens"] ) # 发送 meta_info 包含 ID 和最后一次 agent loop 的 token 使用 yield { "type": "meta_info", "user_msg_id": user_msg_id, "ai_msg_id": ai_msg_id, "last_loop_tokens": last_loop_tokens } except openai.AuthenticationError: yield {"type": "error", "data": lang("API key error or invalid, please check if key is correct")} except openai.RateLimitError as e: yield {"type": "error", "data": lang("Rate limit exceeded, please try again later or increase quota: {}").format(e)} except openai.APIConnectionError as e: yield {"type": "error", "data": lang(f"Cannot connect to API server ({self.base_url}), please check network or address: {e}")} except openai.APIError as e: yield {"type": "error", "data": lang(f"API returned error: {str(e)}")} except Exception as e: yield {"type": "error", "data": lang(f"Unknown error when calling AI interface: {str(e)}")} def _build_messages(self, context_str: str = "") -> List[Dict[str, Any]]: """构建包含系统指令和全量历史的 Prompt。""" if context_str: self.system_prompt += f"\n\n[History Context]:\n{context_str}" messages = [{"role": "system", "content": self.system_prompt}] # 使用 get_sliding_window 获取记忆 # 由于初始化时 sliding_window_size 设置很大,这里实际上获取的是全量或近乎全量的记忆 window = self.memory.get_sliding_window() for msg in window: m = { "role": msg["role"], "content": msg["content"] } # 虽然SimpleAgent不产生工具调用,但如果历史记录里有(比如之前是普通Agent产生的), # 这里兼容一下,防止报错或信息丢失,但对于SimpleAgent新产生的对话不会有这些。 if "tool_calls" in msg: m["tool_calls"] = msg["tool_calls"] if "tool_call_id" in msg: m["tool_call_id"] = msg["tool_call_id"] messages.append(m) return messages