mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[eric] lazy MCP connections + browser agent 9Router support
Performance fix: - MCP servers no longer spawned upfront for every message - Tool schemas loaded from cached metadata (instant, no subprocess) - MCP servers only connected when their tools are first called (lazy) - Browser agent MCP server still connects upfront (lightweight) - Reduces startup from 30+ seconds to <1 second for simple messages Browser agent 9Router support: - Browser agent now works through 9Router when no API key set - Uses OpenAI client format when base_url provided - Model ID mapping for 9Router (cc/ prefix) - Passes auth_token and base_url through run_browser_agents Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0a907e4bac
commit
36d79fd1d3
@@ -649,14 +649,46 @@ class AgentManager:
|
||||
"type": "stdio",
|
||||
}
|
||||
|
||||
# Connect MCP servers and collect tool schemas
|
||||
# Lazy MCP: build tool schemas from saved metadata (no subprocess spawn)
|
||||
# Actual MCP servers are only connected when a tool is first called
|
||||
mcp_manager = MCPClientManager()
|
||||
await mcp_manager.__aenter__()
|
||||
|
||||
mcp_tool_schemas = []
|
||||
for server_name, server_config in mcp_servers.items():
|
||||
tools = await mcp_manager.connect(server_name, server_config)
|
||||
mcp_tool_schemas.extend(tools)
|
||||
from backend.apps.agents.providers.base import ToolSchema
|
||||
|
||||
mcp_tool_schemas: list[ToolSchema] = []
|
||||
_pending_mcp_servers: dict[str, dict] = dict(mcp_servers) # servers not yet connected
|
||||
_connected_servers: set[str] = set()
|
||||
|
||||
# Build tool schemas from saved tool_permissions metadata (instant, no subprocess)
|
||||
for tool in load_all_tools():
|
||||
if not tool.mcp_config or not tool.enabled:
|
||||
continue
|
||||
server_name = _sanitize_server_name(tool.name)
|
||||
if server_name not in _pending_mcp_servers and server_name not in mcp_servers:
|
||||
continue
|
||||
tool_descs = tool.tool_permissions.get("_tool_descriptions", {})
|
||||
denied = _get_denied_tool_names(tool)
|
||||
for tn, desc in tool_descs.items():
|
||||
if tn not in denied:
|
||||
mcp_tool_schemas.append(ToolSchema(
|
||||
name=f"mcp__{server_name}__{tn}",
|
||||
description=desc,
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
))
|
||||
|
||||
# Always add browser agent tools (they're lightweight)
|
||||
browser_agent_name = "openswarm-browser-agent"
|
||||
if browser_agent_name in mcp_servers:
|
||||
try:
|
||||
ba_tools = await asyncio.wait_for(
|
||||
mcp_manager.connect(browser_agent_name, mcp_servers[browser_agent_name]),
|
||||
timeout=10.0,
|
||||
)
|
||||
mcp_tool_schemas.extend(ba_tools)
|
||||
_connected_servers.add(browser_agent_name)
|
||||
except Exception as e:
|
||||
logger.warning(f"Browser agent MCP connection failed: {e}")
|
||||
|
||||
# Collect builtin + MCP tool schemas
|
||||
from backend.apps.agents.tools.registry import get_all_tool_schemas
|
||||
@@ -699,6 +731,19 @@ class AgentManager:
|
||||
parsed = mcp_manager.parse_mcp_tool_name(tool_name)
|
||||
if parsed:
|
||||
server_name, bare_name = parsed
|
||||
|
||||
# Lazy connect: if this server hasn't been connected yet, connect now
|
||||
if server_name not in _connected_servers and server_name in _pending_mcp_servers:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
mcp_manager.connect(server_name, _pending_mcp_servers[server_name]),
|
||||
timeout=15.0,
|
||||
)
|
||||
_connected_servers.add(server_name)
|
||||
except Exception as e:
|
||||
logger.warning(f"Lazy MCP connect failed for {server_name}: {e}")
|
||||
return [{"type": "text", "text": f"Failed to connect to {server_name}: {e}"}]
|
||||
|
||||
result = await mcp_manager.call_tool(server_name, bare_name, tool_input)
|
||||
elapsed_ms = int((time.time() - t0) * 1000)
|
||||
_analytics("tool.called", {
|
||||
|
||||
@@ -278,6 +278,8 @@ async def run_browser_agent(
|
||||
pre_selected: bool = False,
|
||||
initial_url: str | None = None,
|
||||
parent_session_id: str | None = None,
|
||||
auth_token: str | None = None,
|
||||
base_url: str | None = None,
|
||||
) -> dict:
|
||||
"""Run a browser sub-agent loop for a single browser card.
|
||||
|
||||
@@ -315,7 +317,17 @@ async def run_browser_agent(
|
||||
logger.info(f"Browser agent {session_id}: navigated to {initial_url}: {nav_result.get('text', nav_result.get('error', ''))}")
|
||||
|
||||
api_model = MODEL_MAP.get(model, model)
|
||||
client = anthropic.AsyncAnthropic(api_key=api_key)
|
||||
|
||||
# Use OpenAI client for 9Router, Anthropic client for direct API
|
||||
_use_openai_client = base_url is not None
|
||||
if _use_openai_client:
|
||||
from openai import AsyncOpenAI
|
||||
# Map to 9Router model IDs
|
||||
_9r_map = {"sonnet": "cc/claude-sonnet-4-6", "opus": "cc/claude-opus-4-6", "haiku": "cc/claude-haiku-4-5-20251001"}
|
||||
api_model = _9r_map.get(model, f"cc/{api_model}" if not api_model.startswith("cc/") else api_model)
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
else:
|
||||
client = anthropic.AsyncAnthropic(api_key=api_key)
|
||||
|
||||
messages: list[dict] = [{"role": "user", "content": task}]
|
||||
action_log: list[dict] = []
|
||||
@@ -330,30 +342,66 @@ async def run_browser_agent(
|
||||
|
||||
try:
|
||||
for turn in range(MAX_TURNS):
|
||||
response = await client.messages.create(
|
||||
model=api_model,
|
||||
max_tokens=4096,
|
||||
system=SYSTEM_PROMPT,
|
||||
tools=BROWSER_TOOLS_SCHEMA,
|
||||
messages=messages,
|
||||
)
|
||||
if _use_openai_client:
|
||||
# OpenAI-compatible format (9Router)
|
||||
import json as _json
|
||||
oai_tools = [{"type": "function", "function": {"name": t["name"], "description": t["description"], "parameters": t["input_schema"]}} for t in BROWSER_TOOLS_SCHEMA]
|
||||
oai_messages = [{"role": "system", "content": SYSTEM_PROMPT}] + messages
|
||||
resp = await client.chat.completions.create(model=api_model, max_tokens=4096, tools=oai_tools, messages=oai_messages)
|
||||
choice = resp.choices[0]
|
||||
|
||||
assistant_content = []
|
||||
text_parts = []
|
||||
tool_uses = []
|
||||
assistant_content = []
|
||||
text_parts = []
|
||||
tool_uses = []
|
||||
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
text_parts.append(block.text)
|
||||
assistant_content.append({"type": "text", "text": block.text})
|
||||
elif block.type == "tool_use":
|
||||
tool_uses.append(block)
|
||||
assistant_content.append({
|
||||
"type": "tool_use",
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"input": block.input,
|
||||
})
|
||||
if choice.message.content:
|
||||
text_parts.append(choice.message.content)
|
||||
assistant_content.append({"type": "text", "text": choice.message.content})
|
||||
|
||||
if choice.message.tool_calls:
|
||||
for tc in choice.message.tool_calls:
|
||||
try:
|
||||
inp = _json.loads(tc.function.arguments)
|
||||
except Exception:
|
||||
inp = {}
|
||||
# Create a simple object with .id, .name, .input
|
||||
class _TC:
|
||||
pass
|
||||
tool_obj = _TC()
|
||||
tool_obj.id = tc.id
|
||||
tool_obj.name = tc.function.name
|
||||
tool_obj.input = inp
|
||||
tool_uses.append(tool_obj)
|
||||
assistant_content.append({"type": "tool_use", "id": tc.id, "name": tc.function.name, "input": inp})
|
||||
|
||||
stop_reason = "tool_use" if choice.message.tool_calls else "end_turn"
|
||||
else:
|
||||
# Anthropic format (direct API)
|
||||
response = await client.messages.create(
|
||||
model=api_model,
|
||||
max_tokens=4096,
|
||||
system=SYSTEM_PROMPT,
|
||||
tools=BROWSER_TOOLS_SCHEMA,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
assistant_content = []
|
||||
text_parts = []
|
||||
tool_uses = []
|
||||
stop_reason = response.stop_reason
|
||||
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
text_parts.append(block.text)
|
||||
assistant_content.append({"type": "text", "text": block.text})
|
||||
elif block.type == "tool_use":
|
||||
tool_uses.append(block)
|
||||
assistant_content.append({
|
||||
"type": "tool_use",
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"input": block.input,
|
||||
})
|
||||
|
||||
if text_parts:
|
||||
asst_msg = Message(
|
||||
@@ -377,9 +425,16 @@ async def run_browser_agent(
|
||||
"message": tool_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
messages.append({"role": "assistant", "content": assistant_content})
|
||||
if _use_openai_client:
|
||||
# OpenAI format: assistant message with tool_calls
|
||||
asst_api_msg: dict = {"role": "assistant", "content": choice.message.content}
|
||||
if choice.message.tool_calls:
|
||||
asst_api_msg["tool_calls"] = [{"id": tc.id, "type": "function", "function": {"name": tc.function.name, "arguments": tc.function.arguments}} for tc in (choice.message.tool_calls or [])]
|
||||
messages.append(asst_api_msg)
|
||||
else:
|
||||
messages.append({"role": "assistant", "content": assistant_content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
if stop_reason != "tool_use":
|
||||
break
|
||||
|
||||
tool_results = []
|
||||
@@ -460,7 +515,16 @@ async def run_browser_agent(
|
||||
"message": result_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
messages.append({"role": "user", "content": tool_results})
|
||||
if _use_openai_client:
|
||||
# OpenAI format: each tool result is a separate message
|
||||
for tr in tool_results:
|
||||
text_content = ""
|
||||
for block in (tr.get("content") or []):
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text_content += block.get("text", "")
|
||||
messages.append({"role": "tool", "tool_call_id": tr["tool_use_id"], "content": text_content or "Done."})
|
||||
else:
|
||||
messages.append({"role": "user", "content": tool_results})
|
||||
|
||||
summary_parts = text_parts if text_parts else ["Task completed."]
|
||||
summary = "\n".join(summary_parts)
|
||||
@@ -563,6 +627,8 @@ async def run_browser_agents(
|
||||
dashboard_id: str | None = None,
|
||||
pre_selected_browser_ids: list[str] | None = None,
|
||||
parent_session_id: str | None = None,
|
||||
auth_token: str | None = None,
|
||||
base_url: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""Run multiple browser sub-agents in parallel.
|
||||
|
||||
@@ -590,6 +656,8 @@ async def run_browser_agents(
|
||||
pre_selected=is_pre_selected,
|
||||
initial_url=url if url and browser_id not in pre_selected else None,
|
||||
parent_session_id=parent_session_id,
|
||||
auth_token=auth_token,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*[_run_one(t) for t in tasks], return_exceptions=True)
|
||||
|
||||
+18
-3
@@ -145,16 +145,31 @@ async def browser_agent_run(request: Request):
|
||||
return JSONResponse({"error": "tasks array is required"}, status_code=400)
|
||||
|
||||
settings = load_settings()
|
||||
if not settings.anthropic_api_key:
|
||||
return JSONResponse({"error": "Anthropic API key not configured"}, status_code=400)
|
||||
|
||||
# Determine API credentials — check API key, then 9Router
|
||||
api_key = settings.anthropic_api_key
|
||||
auth_token = None
|
||||
base_url = None
|
||||
|
||||
if not api_key:
|
||||
# Try 9Router
|
||||
from backend.apps.nine_router import is_running as _9r_running
|
||||
if _9r_running():
|
||||
api_key = "9router"
|
||||
base_url = "http://localhost:20128/v1"
|
||||
auth_token = None
|
||||
else:
|
||||
return JSONResponse({"error": "No AI provider configured. Set an API key or connect a subscription."}, status_code=400)
|
||||
|
||||
results = await run_browser_agents(
|
||||
tasks=tasks,
|
||||
model=model,
|
||||
api_key=settings.anthropic_api_key,
|
||||
api_key=api_key,
|
||||
dashboard_id=dashboard_id or None,
|
||||
pre_selected_browser_ids=pre_selected_browser_ids,
|
||||
parent_session_id=parent_session_id or None,
|
||||
auth_token=auth_token,
|
||||
base_url=base_url,
|
||||
)
|
||||
return JSONResponse({"results": results})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user