From 624917fac4a8aad2fd159ed861bbe9e598c901fb Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sat, 31 Jan 2026 16:53:33 +0800 Subject: [PATCH] fix: memory and path bug --- agent/memory/summarizer.py | 4 ++-- agent/prompt/builder.py | 5 ++--- agent/protocol/agent_stream.py | 19 ++++++++++++---- agent/tools/ls/ls.py | 4 ++-- agent/tools/read/read.py | 4 ++-- bot/gemini/google_gemini_bot.py | 3 +-- bot/linkai/link_ai_bot.py | 40 +++++++++++++++++++++++---------- bridge/agent_bridge.py | 24 +++++++++++++++----- 8 files changed, 71 insertions(+), 32 deletions(-) diff --git a/agent/memory/summarizer.py b/agent/memory/summarizer.py index 020164e..4b102ab 100644 --- a/agent/memory/summarizer.py +++ b/agent/memory/summarizer.py @@ -117,7 +117,7 @@ class MemoryFlushManager: return user_dir / "MEMORY.md" else: # Return workspace root MEMORY.md - return Path(self.workspace_root) / "MEMORY.md" + return Path(self.workspace_dir) / "MEMORY.md" def create_flush_prompt(self) -> str: """ @@ -214,7 +214,7 @@ def create_memory_files_if_needed(workspace_dir: Path, user_id: Optional[str] = user_dir.mkdir(parents=True, exist_ok=True) main_memory = user_dir / "MEMORY.md" else: - main_memory = Path(workspace_root) / "MEMORY.md" + main_memory = Path(workspace_dir) / "MEMORY.md" if not main_memory.exists(): # Create empty file or with minimal structure (no obvious "Memory" header) diff --git a/agent/prompt/builder.py b/agent/prompt/builder.py index 5086960..a5c99c3 100644 --- a/agent/prompt/builder.py +++ b/agent/prompt/builder.py @@ -378,7 +378,7 @@ def _build_workspace_section(workspace_dir: str, language: str) -> List[str]: "", "**首次对话**:", "", - "如果这是你与用户的首次对话,并且你的人格设定和用户信息还是空白或初始状态:", + "如果这是你与用户的首次对话,并你的`SOUL.md`和`USER.md`均是完全空白或初始模板状态的时候才会进行以下流程:", "", "1. **表达初次启动的感觉** - 像是第一次睁开眼看到世界,带着好奇和期待", "2. **简短打招呼后,分点询问三个核心问题**:", @@ -391,8 +391,7 @@ def _build_workspace_section(workspace_dir: str, language: str) -> List[str]: "", "**重要**: ", "- 在所有对话中,无需提及技术细节(如 SOUL.md、USER.md 等文件名,工具名称,配置等),除非用户明确询问。用自然表达如「我已记住」而非「已更新 SOUL.md」", - "- 不要问太多其他信息(职业、时区等可以后续自然了解)", - "- 保持简洁,避免过度抒情", + "- 不要问太多其他信息(职业、时区等可以后续自然了解),只要`SOUL.md`和`USER.md`又被填写过真实内容而不是占位则说明已经不是首次对话了,此时不用进行初始流程", "", ] diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index d197023..95ad64f 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -305,10 +305,20 @@ class AgentStreamExecutor: for chunk in stream: # Check for errors if isinstance(chunk, dict) and chunk.get("error"): - error_msg = chunk.get("message", "Unknown error") + # Extract error message from nested structure + error_data = chunk.get("error", {}) + if isinstance(error_data, dict): + error_msg = error_data.get("message", chunk.get("message", "Unknown error")) + error_code = error_data.get("code", "") + else: + error_msg = chunk.get("message", str(error_data)) + error_code = "" + status_code = chunk.get("status_code", "N/A") - logger.error(f"API Error: {error_msg} (Status: {status_code})") + logger.error(f"API Error: {error_msg} (Status: {status_code}, Code: {error_code})") logger.error(f"Full error chunk: {chunk}") + + # Raise exception with full error message for retry logic raise Exception(f"{error_msg} (Status: {status_code})") # Parse chunk @@ -346,10 +356,11 @@ class AgentStreamExecutor: except Exception as e: error_str = str(e).lower() - # Check if error is retryable (timeout, connection, rate limit, etc.) + # Check if error is retryable (timeout, connection, rate limit, server busy, etc.) is_retryable = any(keyword in error_str for keyword in [ 'timeout', 'timed out', 'connection', 'network', - 'rate limit', 'overloaded', 'unavailable', '429', '500', '502', '503', '504' + 'rate limit', 'overloaded', 'unavailable', 'busy', 'retry', + '429', '500', '502', '503', '504', '512' ]) if is_retryable and retry_count < max_retries: diff --git a/agent/tools/ls/ls.py b/agent/tools/ls/ls.py index 35e401b..d3e5330 100644 --- a/agent/tools/ls/ls.py +++ b/agent/tools/ls/ls.py @@ -23,7 +23,7 @@ class Ls(BaseTool): "properties": { "path": { "type": "string", - "description": "Directory to list (default: current directory)" + "description": "Directory to list. IMPORTANT: Relative paths are based on workspace directory. To access directories outside workspace, use absolute paths starting with ~ or /." }, "limit": { "type": "integer", @@ -56,7 +56,7 @@ class Ls(BaseTool): return ToolResult.fail( f"Error: Path not found: {path}\n" f"Resolved to: {absolute_path}\n" - f"Hint: If accessing files outside workspace ({self.cwd}), use absolute path like ~/{path} or /full/path/{path}" + f"Hint: Relative paths are based on workspace ({self.cwd}). For files outside workspace, use absolute paths." ) return ToolResult.fail(f"Error: Path not found: {path}") diff --git a/agent/tools/read/read.py b/agent/tools/read/read.py index 9c1d6dc..4810890 100644 --- a/agent/tools/read/read.py +++ b/agent/tools/read/read.py @@ -22,7 +22,7 @@ class Read(BaseTool): "properties": { "path": { "type": "string", - "description": "Path to the file to read (relative or absolute)" + "description": "Path to the file to read. IMPORTANT: Relative paths are based on workspace directory. To access files outside workspace, use absolute paths starting with ~ or /." }, "offset": { "type": "integer", @@ -68,7 +68,7 @@ class Read(BaseTool): return ToolResult.fail( f"Error: File not found: {path}\n" f"Resolved to: {absolute_path}\n" - f"Hint: If accessing files outside workspace ({self.cwd}), use absolute path like ~/{path}" + f"Hint: Relative paths are based on workspace ({self.cwd}). For files outside workspace, use absolute paths." ) return ToolResult.fail(f"Error: File not found: {path}") diff --git a/bot/gemini/google_gemini_bot.py b/bot/gemini/google_gemini_bot.py index e166c30..422ec2e 100644 --- a/bot/gemini/google_gemini_bot.py +++ b/bot/gemini/google_gemini_bot.py @@ -245,8 +245,7 @@ class GoogleGeminiBot(Bot): gen_config = {} if kwargs.get("temperature") is not None: gen_config["temperature"] = kwargs["temperature"] - if kwargs.get("max_tokens"): - gen_config["maxOutputTokens"] = kwargs["max_tokens"] + if gen_config: payload["generationConfig"] = gen_config diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index f356148..c94a72c 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -4,6 +4,7 @@ import re import time import requests +import json import config from bot.bot import Bot from bot.openai_compatible_bot import OpenAICompatibleBot @@ -463,7 +464,7 @@ class LinkAISessionManager(SessionManager): session.add_query(query) session.add_reply(reply) try: - max_tokens = conf().get("conversation_max_tokens", 2500) + max_tokens = conf().get("conversation_max_tokens", 8000) tokens_cnt = session.discard_exceeding(max_tokens, total_tokens) logger.debug(f"[LinkAI] chat history, before tokens={total_tokens}, now tokens={tokens_cnt}") except Exception as e: @@ -504,6 +505,31 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs): Formatted response in OpenAI format or generator for streaming """ try: + # Debug logging + logger.info(f"[LinkAI] ⭐ LinkAI call_with_tools method called") + logger.info(f"[LinkAI] messages count (before conversion): {len(messages) if messages else 0}") + + # Convert messages from Claude format to OpenAI format + # This is important because Agent uses Claude format internally + messages = self._convert_messages_to_openai_format(messages) + logger.info(f"[LinkAI] messages count (after conversion): {len(messages) if messages else 0}") + + # Convert tools from Claude format to OpenAI format + if tools: + tools = self._convert_tools_to_openai_format(tools) + + # Handle system prompt (OpenAI uses system message, Claude uses separate parameter) + system_prompt = kwargs.get('system') + if system_prompt: + # Add system message at the beginning if not already present + if not messages or messages[0].get('role') != 'system': + messages = [{"role": "system", "content": system_prompt}] + messages + else: + # Replace existing system message + messages[0] = {"role": "system", "content": system_prompt} + + logger.info(f"[LinkAI] Final messages count: {len(messages)}, tools count: {len(tools) if tools else 0}, stream: {stream}") + # Build request parameters (LinkAI uses OpenAI-compatible format) body = { "messages": messages, @@ -514,17 +540,7 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs): "presence_penalty": kwargs.get("presence_penalty", conf().get("presence_penalty", 0.0)), "stream": stream } - - # Add max_tokens if specified - if kwargs.get("max_tokens"): - body["max_tokens"] = kwargs["max_tokens"] - - # Add app_code if provided - app_code = kwargs.get("app_code", conf().get("linkai_app_code")) - if app_code: - body["app_code"] = app_code - - # Add tools if provided (OpenAI-compatible format) + if tools: body["tools"] = tools body["tool_choice"] = kwargs.get("tool_choice", "auto") diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 9be9e17..43a9126 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -66,14 +66,20 @@ class AgentLLMModel(LLMModel): self.bridge = bridge self.bot_type = bot_type self._bot = None + self._use_linkai = conf().get("use_linkai", False) and conf().get("linkai_api_key") @property def bot(self): """Lazy load the bot and enhance it with tool calling if needed""" if self._bot is None: - self._bot = self.bridge.get_bot(self.bot_type) - # Automatically add tool calling support if not present - self._bot = add_openai_compatible_support(self._bot) + # If use_linkai is enabled, use LinkAI bot directly + if self._use_linkai: + logger.info("[AgentBridge] Using LinkAI bot for agent") + self._bot = self.bridge.find_chat_bot(const.LINKAI) + else: + self._bot = self.bridge.get_bot(self.bot_type) + # Automatically add tool calling support if not present + self._bot = add_openai_compatible_support(self._bot) return self._bot def call(self, request: LLMRequest): @@ -88,11 +94,18 @@ class AgentLLMModel(LLMModel): kwargs = { 'messages': request.messages, 'tools': getattr(request, 'tools', None), - 'stream': False + 'stream': False, + 'model': self.model # Pass model parameter } # Only pass max_tokens if it's explicitly set if request.max_tokens is not None: kwargs['max_tokens'] = request.max_tokens + + # Extract system prompt if present + system_prompt = getattr(request, 'system', None) + if system_prompt: + kwargs['system'] = system_prompt + response = self.bot.call_with_tools(**kwargs) return self._format_response(response) else: @@ -122,7 +135,8 @@ class AgentLLMModel(LLMModel): 'messages': request.messages, 'tools': getattr(request, 'tools', None), 'stream': True, - 'max_tokens': max_tokens + 'max_tokens': max_tokens, + 'model': self.model # Pass model parameter } # Add system prompt if present