diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index 2cd89df..f8b2df1 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -171,54 +171,75 @@ class AgentStreamExecutor: tool_results = [] tool_result_blocks = [] - for tool_call in tool_calls: - result = self._execute_tool(tool_call) - tool_results.append(result) - - # Log tool result in compact format - status_emoji = "✅" if result.get("status") == "success" else "❌" - result_data = result.get('result', '') - # Format result string with proper Chinese character support - if isinstance(result_data, (dict, list)): - result_str = json.dumps(result_data, ensure_ascii=False) - else: - result_str = str(result_data) - logger.info(f" {status_emoji} {tool_call['name']} ({result.get('execution_time', 0):.2f}s): {result_str[:200]}{'...' if len(result_str) > 200 else ''}") + try: + for tool_call in tool_calls: + result = self._execute_tool(tool_call) + tool_results.append(result) + + # Log tool result in compact format + status_emoji = "✅" if result.get("status") == "success" else "❌" + result_data = result.get('result', '') + # Format result string with proper Chinese character support + if isinstance(result_data, (dict, list)): + result_str = json.dumps(result_data, ensure_ascii=False) + else: + result_str = str(result_data) + logger.info(f" {status_emoji} {tool_call['name']} ({result.get('execution_time', 0):.2f}s): {result_str[:200]}{'...' if len(result_str) > 200 else ''}") - # Build tool result block (Claude format) - # Format content in a way that's easy for LLM to understand - is_error = result.get("status") == "error" - - if is_error: - # For errors, provide clear error message - result_content = f"Error: {result.get('result', 'Unknown error')}" - elif isinstance(result.get('result'), dict): - # For dict results, use JSON format - result_content = json.dumps(result.get('result'), ensure_ascii=False) - elif isinstance(result.get('result'), str): - # For string results, use directly - result_content = result.get('result') - else: - # Fallback to full JSON - result_content = json.dumps(result, ensure_ascii=False) - - tool_result_block = { - "type": "tool_result", - "tool_use_id": tool_call["id"], - "content": result_content - } - - # Add is_error field for Claude API (helps model understand failures) - if is_error: - tool_result_block["is_error"] = True - - tool_result_blocks.append(tool_result_block) - - # Add tool results to message history as user message (Claude format) - self.messages.append({ - "role": "user", - "content": tool_result_blocks - }) + # Build tool result block (Claude format) + # Format content in a way that's easy for LLM to understand + is_error = result.get("status") == "error" + + if is_error: + # For errors, provide clear error message + result_content = f"Error: {result.get('result', 'Unknown error')}" + elif isinstance(result.get('result'), dict): + # For dict results, use JSON format + result_content = json.dumps(result.get('result'), ensure_ascii=False) + elif isinstance(result.get('result'), str): + # For string results, use directly + result_content = result.get('result') + else: + # Fallback to full JSON + result_content = json.dumps(result, ensure_ascii=False) + + tool_result_block = { + "type": "tool_result", + "tool_use_id": tool_call["id"], + "content": result_content + } + + # Add is_error field for Claude API (helps model understand failures) + if is_error: + tool_result_block["is_error"] = True + + tool_result_blocks.append(tool_result_block) + + finally: + # CRITICAL: Always add tool_result to maintain message history integrity + # Even if tool execution fails, we must add error results to match tool_use + if tool_result_blocks: + # Add tool results to message history as user message (Claude format) + self.messages.append({ + "role": "user", + "content": tool_result_blocks + }) + elif tool_calls: + # If we have tool_calls but no tool_result_blocks (unexpected error), + # create error results for all tool calls to maintain message integrity + logger.warning("⚠️ Tool execution interrupted, adding error results to maintain message history") + emergency_blocks = [] + for tool_call in tool_calls: + emergency_blocks.append({ + "type": "tool_result", + "tool_use_id": tool_call["id"], + "content": "Error: Tool execution was interrupted", + "is_error": True + }) + self.messages.append({ + "role": "user", + "content": emergency_blocks + }) self._emit_event("turn_end", { "turn": turn, @@ -257,6 +278,9 @@ class AgentStreamExecutor: Returns: (response_text, tool_calls) """ + # Validate and fix message history first + self._validate_and_fix_messages() + # Trim messages if needed (using agent's context management) self._trim_messages() @@ -513,6 +537,27 @@ class AgentStreamExecutor: }) return error_result + def _validate_and_fix_messages(self): + """ + Validate message history and fix incomplete tool_use/tool_result pairs. + Claude API requires each tool_use to have a corresponding tool_result immediately after. + """ + if not self.messages: + return + + # Check last message for incomplete tool_use + if len(self.messages) > 0: + last_msg = self.messages[-1] + if last_msg.get("role") == "assistant": + # Check if assistant message has tool_use blocks + content = last_msg.get("content", []) + if isinstance(content, list): + has_tool_use = any(block.get("type") == "tool_use" for block in content) + if has_tool_use: + # This is incomplete - remove it + logger.warning(f"⚠️ Removing incomplete tool_use message from history") + self.messages.pop() + def _trim_messages(self): """ Trim message history to stay within context limits. diff --git a/agent/tools/__init__.py b/agent/tools/__init__.py index a8863ce..a749633 100644 --- a/agent/tools/__init__.py +++ b/agent/tools/__init__.py @@ -21,6 +21,7 @@ from agent.tools.memory.memory_get import MemoryGetTool # Import web tools from agent.tools.web_fetch.web_fetch import WebFetch +from agent.tools.bocha_search.bocha_search import BochaSearch # Import tools with optional dependencies def _import_optional_tools(): @@ -93,6 +94,7 @@ __all__ = [ 'MemorySearchTool', 'MemoryGetTool', 'WebFetch', + 'BochaSearch', # Optional tools (may be None if dependencies not available) 'GoogleSearch', 'FileSave', diff --git a/agent/tools/web_fetch/web_fetch.py b/agent/tools/web_fetch/web_fetch.py index 8ad11dc..b87b95e 100644 --- a/agent/tools/web_fetch/web_fetch.py +++ b/agent/tools/web_fetch/web_fetch.py @@ -46,7 +46,7 @@ class WebFetch(BaseTool): def __init__(self, config: dict = None): self.config = config or {} - self.timeout = self.config.get("timeout", 30) + self.timeout = self.config.get("timeout", 20) self.max_redirects = self.config.get("max_redirects", 3) self.user_agent = self.config.get( "user_agent", diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 1fbec40..135c1a4 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -171,13 +171,15 @@ class AgentLLMModel(LLMModel): class AgentBridge: """ - Bridge class that integrates single super Agent with COW + Bridge class that integrates super Agent with COW + Manages multiple agent instances per session for conversation isolation """ def __init__(self, bridge: Bridge): self.bridge = bridge + self.agents = {} # session_id -> Agent instance mapping + self.default_agent = None # For backward compatibility (no session_id) self.agent: Optional[Agent] = None - def create_agent(self, system_prompt: str, tools: List = None, **kwargs) -> Agent: """ Create the super agent with COW integration @@ -209,8 +211,8 @@ class AgentBridge: except Exception as e: logger.warning(f"[AgentBridge] Failed to load tool {tool_name}: {e}") - # Create the single super agent - self.agent = Agent( + # Create agent instance + agent = Agent( system_prompt=system_prompt, description=kwargs.get("description", "AI Super Agent"), model=model, @@ -225,21 +227,38 @@ class AgentBridge: ) # Log skill loading details - if self.agent.skill_manager: + if agent.skill_manager: logger.info(f"[AgentBridge] SkillManager initialized:") - logger.info(f"[AgentBridge] - Managed dir: {self.agent.skill_manager.managed_skills_dir}") - logger.info(f"[AgentBridge] - Workspace dir: {self.agent.skill_manager.workspace_dir}") - logger.info(f"[AgentBridge] - Total skills: {len(self.agent.skill_manager.skills)}") - for skill_name in self.agent.skill_manager.skills.keys(): + logger.info(f"[AgentBridge] - Managed dir: {agent.skill_manager.managed_skills_dir}") + logger.info(f"[AgentBridge] - Workspace dir: {agent.skill_manager.workspace_dir}") + logger.info(f"[AgentBridge] - Total skills: {len(agent.skill_manager.skills)}") + for skill_name in agent.skill_manager.skills.keys(): logger.info(f"[AgentBridge] * {skill_name}") - return self.agent + return agent - def get_agent(self) -> Optional[Agent]: - """Get the super agent, create if not exists""" - if self.agent is None: - self._init_default_agent() - return self.agent + def get_agent(self, session_id: str = None) -> Optional[Agent]: + """ + Get agent instance for the given session + + Args: + session_id: Session identifier (e.g., user_id). If None, returns default agent. + + Returns: + Agent instance for this session + """ + # If no session_id, use default agent (backward compatibility) + if session_id is None: + if self.default_agent is None: + self._init_default_agent() + return self.default_agent + + # Check if agent exists for this session + if session_id not in self.agents: + logger.info(f"[AgentBridge] Creating new agent for session: {session_id}") + self._init_agent_for_session(session_id) + + return self.agents[session_id] def _init_default_agent(self): """Initialize default super agent with new prompt system""" @@ -307,6 +326,12 @@ class AgentBridge: tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None) if 'memory_manager' in file_config: tool.memory_manager = file_config['memory_manager'] + # Apply API key for bocha_search tool + elif tool_name == 'bocha_search': + bocha_api_key = conf().get("bocha_api_key", "") + if bocha_api_key: + tool.config = {"bocha_api_key": bocha_api_key} + tool.api_key = bocha_api_key tools.append(tool) logger.debug(f"[AgentBridge] Loaded tool: {tool_name}") except Exception as e: @@ -370,6 +395,127 @@ class AgentBridge: if memory_manager: agent.memory_manager = memory_manager logger.info(f"[AgentBridge] Memory manager attached to agent") + + # Store as default agent + self.default_agent = agent + + def _init_agent_for_session(self, session_id: str): + """ + Initialize agent for a specific session + Reuses the same configuration as default agent + """ + from config import conf + import os + + # Get workspace from config + workspace_root = os.path.expanduser(conf().get("agent_workspace", "~/cow")) + + # Initialize workspace + from agent.prompt import ensure_workspace, load_context_files, PromptBuilder + + workspace_files = ensure_workspace(workspace_root, create_templates=True) + + # Setup memory system + memory_manager = None + memory_tools = [] + + try: + from agent.memory import MemoryManager, MemoryConfig + from agent.tools import MemorySearchTool, MemoryGetTool + + memory_config = MemoryConfig( + workspace_root=workspace_root, + embedding_provider="local", + embedding_model="all-MiniLM-L6-v2" + ) + + memory_manager = MemoryManager(memory_config) + memory_tools = [ + MemorySearchTool(memory_manager), + MemoryGetTool(memory_manager) + ] + + except Exception as e: + logger.debug(f"[AgentBridge] Memory system not available for session {session_id}: {e}") + + # Load tools + from agent.tools import ToolManager + tool_manager = ToolManager() + tool_manager.load_tools() + + tools = [] + file_config = { + "cwd": workspace_root, + "memory_manager": memory_manager + } if memory_manager else {"cwd": workspace_root} + + for tool_name in tool_manager.tool_classes.keys(): + try: + tool = tool_manager.create_tool(tool_name) + if tool: + if tool_name in ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls']: + tool.config = file_config + tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None) + if 'memory_manager' in file_config: + tool.memory_manager = file_config['memory_manager'] + elif tool_name == 'bocha_search': + bocha_api_key = conf().get("bocha_api_key", "") + if bocha_api_key: + tool.config = {"bocha_api_key": bocha_api_key} + tool.api_key = bocha_api_key + tools.append(tool) + except Exception as e: + logger.warning(f"[AgentBridge] Failed to load tool {tool_name} for session {session_id}: {e}") + + if memory_tools: + tools.extend(memory_tools) + + # Load context files + context_files = load_context_files(workspace_root) + + # Check if this is the first conversation + from agent.prompt.workspace import is_first_conversation, mark_conversation_started + is_first = is_first_conversation(workspace_root) + + # Build system prompt + prompt_builder = PromptBuilder( + workspace_dir=workspace_root, + language="zh" + ) + + runtime_info = { + "model": conf().get("model", "unknown"), + "workspace": workspace_root, + "channel": conf().get("channel_type", "unknown") + } + + system_prompt = prompt_builder.build( + tools=tools, + context_files=context_files, + memory_manager=memory_manager, + runtime_info=runtime_info, + is_first_conversation=is_first + ) + + if is_first: + mark_conversation_started(workspace_root) + + # Create agent for this session + agent = self.create_agent( + system_prompt=system_prompt, + tools=tools, + max_steps=50, + output_mode="logger", + workspace_dir=workspace_root, + enable_skills=True + ) + + if memory_manager: + agent.memory_manager = memory_manager + + # Store agent for this session + self.agents[session_id] = agent + logger.info(f"[AgentBridge] Agent created for session: {session_id}") def agent_reply(self, query: str, context: Context = None, on_event=None, clear_history: bool = False) -> Reply: @@ -378,7 +524,7 @@ class AgentBridge: Args: query: User query - context: COW context (optional) + context: COW context (optional, contains session_id for user isolation) on_event: Event callback (optional) clear_history: Whether to clear conversation history @@ -386,8 +532,13 @@ class AgentBridge: Reply object """ try: - # Get agent (will auto-initialize if needed) - agent = self.get_agent() + # Extract session_id from context for user isolation + session_id = None + if context: + session_id = context.kwargs.get("session_id") or context.get("session_id") + + # Get agent for this session (will auto-initialize if needed) + agent = self.get_agent(session_id=session_id) if not agent: return Reply(ReplyType.ERROR, "Failed to initialize super agent") @@ -402,4 +553,21 @@ class AgentBridge: except Exception as e: logger.error(f"Agent reply error: {e}") - return Reply(ReplyType.ERROR, f"Agent error: {str(e)}") \ No newline at end of file + return Reply(ReplyType.ERROR, f"Agent error: {str(e)}") + + def clear_session(self, session_id: str): + """ + Clear a specific session's agent and conversation history + + Args: + session_id: Session identifier to clear + """ + if session_id in self.agents: + logger.info(f"[AgentBridge] Clearing session: {session_id}") + del self.agents[session_id] + + def clear_all_sessions(self): + """Clear all agent sessions""" + logger.info(f"[AgentBridge] Clearing all sessions ({len(self.agents)} total)") + self.agents.clear() + self.default_agent = None \ No newline at end of file diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 5f00f1b..1486c1d 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -49,8 +49,6 @@ class WebChannel(ChatChannel): self.msg_id_counter = 0 # 添加消息ID计数器 self.session_queues = {} # 存储session_id到队列的映射 self.request_to_session = {} # 存储request_id到session_id的映射 - # web channel无需前缀 - conf()["single_chat_prefix"] = [""] def _generate_msg_id(self): @@ -122,18 +120,30 @@ class WebChannel(ChatChannel): if session_id not in self.session_queues: self.session_queues[session_id] = Queue() + # Web channel 不需要前缀,确保消息能通过前缀检查 + trigger_prefixs = conf().get("single_chat_prefix", [""]) + if check_prefix(prompt, trigger_prefixs) is None: + # 如果没有匹配到前缀,给消息加上第一个前缀 + if trigger_prefixs: + prompt = trigger_prefixs[0] + prompt + logger.debug(f"[WebChannel] Added prefix to message: {prompt}") + # 创建消息对象 msg = WebMessage(self._generate_msg_id(), prompt) msg.from_user_id = session_id # 使用会话ID作为用户ID - # 创建上下文 - context = self._compose_context(ContextType.TEXT, prompt, msg=msg) + # 创建上下文,明确指定 isgroup=False + context = self._compose_context(ContextType.TEXT, prompt, msg=msg, isgroup=False) + + # 检查 context 是否为 None(可能被插件过滤等) + if context is None: + logger.warning(f"[WebChannel] Context is None for session {session_id}, message may be filtered") + return json.dumps({"status": "error", "message": "Message was filtered"}) - # 添加必要的字段 + # 覆盖必要的字段(_compose_context 会设置默认值,但我们需要使用实际的 session_id) context["session_id"] = session_id + context["receiver"] = session_id context["request_id"] = request_id - context["isgroup"] = False # 添加 isgroup 字段 - context["receiver"] = session_id # 添加 receiver 字段 # 异步处理消息 - 只传递上下文 threading.Thread(target=self.produce, args=(context,)).start() diff --git a/config.py b/config.py index 1a2a105..0e8625a 100644 --- a/config.py +++ b/config.py @@ -185,7 +185,8 @@ available_setting = { "Minimax_base_url": "", "web_port": 9899, "agent": False, # 是否开启Agent模式 - "agent_workspace": "~/cow" # agent工作空间路径,用于存储skills、memory等 + "agent_workspace": "~/cow", # agent工作空间路径,用于存储skills、memory等 + "bocha_api_key": "" } diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md index 48d335d..3729055 100644 --- a/skills/skill-creator/SKILL.md +++ b/skills/skill-creator/SKILL.md @@ -68,10 +68,11 @@ skill-name/ - Must test scripts before including **references/** - When to include: -- Documentation for agent to reference -- Database schemas, API docs, domain knowledge +- **ONLY** when documentation is too large for SKILL.md (>500 lines) +- Database schemas, complex API specs that agent needs to reference - Agent reads these files into context as needed -- For large files (>10k words), include grep patterns in SKILL.md +- **NOT for**: API reference docs, usage examples, tutorials (put in SKILL.md instead) +- **Rule of thumb**: If it fits in SKILL.md, don't create a separate reference file **assets/** - When to include: - Files used in output (not loaded to context) @@ -82,11 +83,15 @@ skill-name/ ### What NOT to Include -Do NOT create auxiliary documentation: -- README.md -- INSTALLATION_GUIDE.md -- CHANGELOG.md -- Other non-essential files +Do NOT create auxiliary documentation files: +- README.md - Instructions belong in SKILL.md +- INSTALLATION_GUIDE.md - Setup info belongs in SKILL.md +- CHANGELOG.md - Not needed for local skills +- API_REFERENCE.md - Put API docs directly in SKILL.md +- USAGE_EXAMPLES.md - Put examples directly in SKILL.md +- Any other documentation files - Everything goes in SKILL.md unless it's too large + +**Critical Rule**: Only create files that the agent will actually execute (scripts) or that are too large for SKILL.md (references). Documentation, examples, and guides ALL belong in SKILL.md. ## Skill Creation Process @@ -133,22 +138,31 @@ To turn concrete examples into an effective skill, analyze each example by: 1. Considering how to execute on the example from scratch 2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly +**Planning Checklist**: +- ✅ **Always needed**: SKILL.md with clear description and usage instructions +- ✅ **scripts/**: Only if code needs to be executed (not just shown as examples) +- ❌ **references/**: Rarely needed - only if documentation is >500 lines and can't fit in SKILL.md +- ✅ **assets/**: Only if files are used in output (templates, boilerplate, etc.) + Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: 1. Rotating a PDF requires re-writing the same code each time 2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill +3. ❌ Don't create `references/api-docs.md` - put API info in SKILL.md instead Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: 1. Writing a frontend webapp requires the same boilerplate HTML/React each time 2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill +3. ❌ Don't create `references/usage-examples.md` - put examples in SKILL.md instead Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: 1. Querying BigQuery requires re-discovering the table schemas and relationships each time -2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill +2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill (ONLY because schemas are very large) +3. ❌ Don't create separate `references/query-examples.md` - put examples in SKILL.md instead -To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. +To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. **Default to putting everything in SKILL.md unless there's a compelling reason to separate it.** ### Step 3: Initialize the Skill @@ -200,6 +214,12 @@ These files contain established best practices for effective skill design. To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. +**Important Guidelines**: +- **scripts/**: Only create scripts that will be executed. Test all scripts before including. +- **references/**: ONLY create if documentation is too large for SKILL.md (>500 lines). Most skills don't need this. +- **assets/**: Only include files used in output (templates, icons, etc.) +- **Default approach**: Put everything in SKILL.md unless there's a specific reason not to. + Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion. If you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.