diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index c1553483..d99ae4db 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -449,103 +449,6 @@ class AgentManager: logger.info(f"[MCP-DEBUG] Final mcp_servers: {list(mcp_servers.keys())}") return mcp_servers - def _build_connected_tools_context(self, allowed_tools: list[str]) -> str | None: - """Build a context block describing connected MCP tools and their accounts. - - Tools set to 'deny' and fully-denied servers are excluded. - """ - all_tools = load_all_tools() - mcp_tools = [t for t in all_tools if t.mcp_config and t.enabled and t.auth_status in ("configured", "connected")] - - sections = [] - for tool in mcp_tools: - tool_ref = f"mcp:{tool.name}" - if tool_ref not in allowed_tools and allowed_tools != get_all_tool_names(): - continue - - if _is_fully_denied(tool): - continue - - server_name = _sanitize_server_name(tool.name) - denied = _get_denied_tool_names(tool) - tool_descs = { - k: v for k, v in tool.tool_permissions.get("_tool_descriptions", {}).items() - if k not in denied - } - if not tool_descs: - continue - - lines = [f"MCP Server: {server_name}"] - lines.append(f" Status: {tool.auth_status}") - - if tool.connected_account_email: - lines.append(f" Connected account: {tool.connected_account_email}") - lines.append( - f" IMPORTANT: When calling tools from this server that require an email " - f"parameter (e.g. user_google_email, user_email), always use " - f"\"{tool.connected_account_email}\" automatically — do NOT ask the user." - ) - - # Instagram and LinkedIn enforce strict per-account rate limits to - # prevent platform bans. When a tool call comes back with - # rate_limited: true OR a deny reason mentioning "RATE LIMIT HIT", - # the agent MUST stop the task, tell the user the retry-after, and - # NOT retry. Without this guidance, agents tend to loop trying - # alternative tools or even shell out to filesystem search. - if tool.name.lower() in ("instagram", "linkedin", "telegram"): - lines.append( - f" RATE LIMIT BEHAVIOR (HARD RULE): If a {tool.name} tool returns " - "rate_limited: true, or any tool call here returns a 'deny' with " - "'RATE LIMIT HIT' in the message, this is FINAL for the current turn. " - "Do NOT retry the same tool. Do NOT try alternative tools to accomplish " - "the same goal. Do NOT shell out to Bash/curl/find to look up the package " - "source. Tell the user the retry-after time in plain English and END the " - "task. Looping makes the platform ban risk worse, not better." - ) - - # Discord guild scoping — hard restriction. The bot may technically - # be in other servers (across other OpenSwarm users), but this - # specific user only authorized these guild IDs. - if tool.name.lower() == "discord": - guilds = tool.oauth_tokens.get("guilds") or [] - if guilds: - guild_descriptions = ", ".join( - f"{g.get('name', 'Unknown')} ({g.get('id', '')})" for g in guilds - ) - allowed_ids = [g.get("id", "") for g in guilds if g.get("id")] - lines.append( - f" AUTHORIZED DISCORD SERVERS (guild_ids): {guild_descriptions}" - ) - lines.append( - f" HARD RESTRICTION: You MUST only call Discord tools that operate on " - f"these guild_ids: {allowed_ids}. NEVER call Discord tools on any other " - f"guild_id even if the bot has access to it. NEVER list, search, or " - f"enumerate servers outside this list. If a user asks about a server " - f"not in this list, refuse and tell them to authorize it via the Connect " - f"Discord button. This is a security boundary, not a preference." - ) - else: - lines.append( - f" No Discord servers authorized yet. Tell the user to click " - f"'Connect Discord' to add a server before attempting any Discord actions." - ) - - tool_names = list(tool_descs.keys()) - if tool_names: - lines.append(f" Available tools ({len(tool_names)}): {', '.join(tool_names)}") - - sections.append("\n".join(lines)) - - if not sections: - return None - return ( - "\n" - "The following MCP tool servers are connected and available. " - "Use them directly when relevant to the user's request.\n\n" - + "\n\n".join(sections) - + "\n" - ) - def _build_browser_context(self, dashboard_id: str | None, selected_browser_ids: list[str] | None = None) -> str | None: """Build a context block listing browser cards and delegation instructions. @@ -935,72 +838,6 @@ class AgentManager: # surfaces from the catch-all # ------------------------------------------------------------------ - @staticmethod - def _approx_tokens(text: str) -> int: - """Conservative chars/4 estimate. Used for the pre-send guard - and the compaction trigger when a precise count_tokens isn't - cheap (or the route isn't Anthropic). Errs slightly high so we - compact a touch earlier than strictly necessary.""" - return max(1, len(text or "") // 4) - - @staticmethod - def _summarize_message_block(messages: list) -> str: - """Programmatic, no-LLM summary of a message slice. Mirrors the - shape of browser_agent._summarize_messages: extracts the original - user task, counts tool calls, captures the last assistant text. - Cheap, deterministic, and never makes a network call — so - compaction itself adds zero latency to the user's turn. - """ - if not messages: - return "" - - initial_task = "" - for m in messages: - if getattr(m, "role", "") == "user": - content = getattr(m, "content", "") - txt = content if isinstance(content, str) else str(content) - if txt.strip(): - initial_task = txt.strip()[:400] - break - - tool_calls_by_name: dict[str, int] = {} - last_tool_results = 0 - last_assistant_text = "" - for m in messages: - role = getattr(m, "role", "") - if role == "tool_call": - content = getattr(m, "content", {}) or {} - name = (content.get("tool") if isinstance(content, dict) else None) or "unknown" - tool_calls_by_name[name] = tool_calls_by_name.get(name, 0) + 1 - elif role == "tool_result": - last_tool_results += 1 - elif role == "assistant": - content = getattr(m, "content", "") - if isinstance(content, str) and content.strip(): - last_assistant_text = content.strip() - elif isinstance(content, list): - for block in content: - if isinstance(block, dict) and block.get("type") == "text": - txt = (block.get("text") or "").strip() - if txt: - last_assistant_text = txt - - parts = [""] - parts.append("[The following is a programmatic summary of earlier turns in this session. Originals are preserved on disk and viewable via the chat UI's compaction drawer.]") - if initial_task: - parts.append(f'Initial user request: "{initial_task}"') - if tool_calls_by_name: - total = sum(tool_calls_by_name.values()) - top = sorted(tool_calls_by_name.items(), key=lambda kv: -kv[1])[:8] - parts.append(f"Tool calls so far ({total} total): " + ", ".join(f"{n}×{c}" for n, c in top)) - if last_tool_results: - parts.append(f"Tool results received: {last_tool_results}") - if last_assistant_text: - parts.append("Last assistant message:") - parts.append(last_assistant_text[:1200]) - parts.append("") - return "\n".join(parts) - def _maybe_compact(self, session: AgentSession, force: bool = False) -> bool: """Run summarizer when ctx_used_pct >= compact_threshold_pct (or force).