diff --git a/backend/apps/agents/COMMS_MANAGER/COMMS_MANAGER.py b/backend/apps/agents/COMMS_MANAGER/COMMS_MANAGER.py index fd314c83..cf49b3f9 100644 --- a/backend/apps/agents/COMMS_MANAGER/COMMS_MANAGER.py +++ b/backend/apps/agents/COMMS_MANAGER/COMMS_MANAGER.py @@ -3,8 +3,8 @@ from uuid import uuid4 from pydantic import BaseModel, Field from typeguard import typechecked -from backend.apps.agents.utils.comms_utils.classes.FutureBridge import FutureBridge -from backend.apps.agents.utils.comms_utils.classes.FrontendBroadcaster import FrontendBroadcaster +from backend.apps.agents.COMMS_MANAGER.classes.FutureBridge import FutureBridge +from backend.apps.agents.COMMS_MANAGER.classes.FrontendBroadcaster import FrontendBroadcaster from backend.core.events.events import AnyEvent, ApprovalRequestEvent, EventCallback diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index d4815c04..abe8c6fe 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -28,6 +28,9 @@ from backend.core.tools.make_builtin_toolkit.make_builtin_toolkit import make_bu from backend.apps.agents.agent_utils.create_sdk_hooks import create_sdk_hooks from backend.apps.agents.agent_utils.build_search_text import build_search_text from backend.apps.agents.COMMS_MANAGER.COMMS_MANAGER import COMMS_MANAGER +from backend.apps.settings.settings import load_settings +from backend.core.llm.resolve_sdk_env import resolve_sdk_env +from backend.ports import NINE_ROUTER_PORT from claude_agent_sdk import ClaudeAgentOptions from claude_agent_sdk.types import HookMatcher, McpServerConfig from backend.core.tools.shared_structs.Toolkit import Toolkit @@ -118,7 +121,15 @@ async def launch(body: LaunchBody) -> dict: can_use_tool, pre_tool_hook, post_tool_hook = create_sdk_hooks(agent) + settings = load_settings() + env = resolve_sdk_env( + api_key=settings.anthropic_api_key, + nine_router_port=NINE_ROUTER_PORT if not settings.anthropic_api_key else None, + ) + agent.config = ClaudeAgentOptions( + env=env, + model=body.model, system_prompt=system_prompt, max_turns=body.max_turns, mcp_servers=mcp_servers if mcp_servers else None, diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index cd006260..5b7fca64 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -1,13 +1,11 @@ import json import os -import logging from contextlib import asynccontextmanager from datetime import datetime from uuid import uuid4 from pydantic import BaseModel from backend.config.Apps import SubApp -# from backend.apps.common.json_store import JsonStore from backend.core.db.PydanticStore import PydanticStore from backend.apps.dashboards.Dashboard import ( Dashboard, @@ -15,16 +13,9 @@ from backend.apps.dashboards.Dashboard import ( ) from backend.apps.agents.agents import list_sessions, delete_session from backend.apps.settings.settings import load_settings -from backend.core.llm.quick_llm_call import quick_llm_call +from backend.apps.dashboards.generate_dashboard_name import generate_dashboard_name from backend.ports import NINE_ROUTER_PORT from typing import Optional - - - - -logger = logging.getLogger(__name__) - -# from backend.config.paths import DASHBOARDS_DIR as DATA_DIR, SESSIONS_DIR from backend.config.paths import DB_ROOT @@ -75,7 +66,6 @@ async def create_dashboard(body: DashboardCreate): # TODO: Maybe parse the output of list_sessions into actual Agent objects? @dashboards.router.post("/{dashboard_id}/generate-name") async def generate_name(dashboard_id: str): - dashboard = DASHBOARD_STORE.load(dashboard_id) if not dashboard.auto_named and dashboard.name != "Untitled Dashboard": @@ -94,33 +84,18 @@ async def generate_name(dashboard_id: str): if not prompts: return {"name": dashboard.name, "auto_named": dashboard.auto_named} + settings = load_settings() fallback = prompts[0][:40] try: - if len(prompts) == 1: - system = "Generate a concise 2-5 word workspace name for a project based on this task. Return only the name, nothing else." - user_content = prompts[0] - else: - system = "Generate a concise 2-5 word workspace name that captures the overall theme of these tasks. Return only the name, nothing else." - user_content = "\n".join(f"- {p}" for p in prompts) - - settings = load_settings() - llm_kwargs = {} - if settings.anthropic_api_key: - llm_kwargs["api_key"] = settings.anthropic_api_key - else: - llm_kwargs["nine_router_port"] = NINE_ROUTER_PORT - - generated = await quick_llm_call( - system, user_content, - model="claude-haiku-4-5-20251001", - max_tokens=30, - **llm_kwargs, + generated = await generate_dashboard_name( + prompts=prompts, + api_key=settings.anthropic_api_key, + nine_router_port=NINE_ROUTER_PORT, ) - generated = generated.strip('"\'') if generated: fallback = generated except Exception as e: - logger.warning(f"Dashboard name generation failed, using fallback: {e}") + print(f"[dashboards.generate_name] ERROR: Dashboard name generation failed, using fallback: {e}") dashboard.name = fallback dashboard.auto_named = True @@ -166,7 +141,7 @@ async def delete_dashboard(dashboard_id: str): try: await delete_session(session["session_id"]) except Exception: - logger.warning(f"Failed to delete session {session.get('session_id')} during dashboard deletion") + print(f"[dashboards.delete_dashboard] ERROR: Failed to delete session {session.get('session_id')} during dashboard deletion") DASHBOARD_STORE.delete(dashboard_id) return {"ok": True} diff --git a/backend/apps/dashboards/generate_dashboard_name.py b/backend/apps/dashboards/generate_dashboard_name.py new file mode 100644 index 00000000..5e8e012f --- /dev/null +++ b/backend/apps/dashboards/generate_dashboard_name.py @@ -0,0 +1,30 @@ +from typing import List, Optional +from backend.core.llm.quick_llm_call import quick_llm_call +from typeguard import typechecked + +SINGLE_AGENT_SYSTEM_PROMPT = "Generate a concise 2-5 word workspace name for a project based on this task. Return only the name with no markdown formatting, nothing else." +MULTI_AGENT_SYSTEM_PROMPT = "Generate a concise 2-5 word workspace name that captures the overall theme of these tasks. Return only the name with no markdown formatting, nothing else." + +@typechecked +async def generate_dashboard_name( + prompts: List[str], + api_key: Optional[str] = None, + nine_router_port: Optional[int] = None, +) -> str: + """Given user prompts from a dashboard's sessions, generate a short name via LLM.""" + if len(prompts) == 1: + system_prompt = SINGLE_AGENT_SYSTEM_PROMPT + user_prompt = prompts[0] + else: + system_prompt = MULTI_AGENT_SYSTEM_PROMPT + user_prompt = "\n".join(f"- {p}" for p in prompts) + + result = await quick_llm_call( + api_key=api_key, + nine_router_port=nine_router_port, + system_prompt=system_prompt, + user_prompt=user_prompt, + model="claude-haiku-4-5-20251001", + max_tokens=30, + ) + return result.strip("\"'") \ No newline at end of file diff --git a/backend/core/llm/quick_llm_call.py b/backend/core/llm/quick_llm_call.py index 7dd76409..8e283a1c 100644 --- a/backend/core/llm/quick_llm_call.py +++ b/backend/core/llm/quick_llm_call.py @@ -1,20 +1,25 @@ -from backend.core.llm.get_llm_client import get_llm_client +from backend.core.llm.resolve_sdk_env import resolve_sdk_env, SDK_ENV_DICT from typing import Optional from anthropic.types import Message as AnthropicMessage +import anthropic async def quick_llm_call( - system: str, - user_content: str, + system_prompt: str, + user_prompt: str, model: str, max_tokens: int, api_key: Optional[str] = None, nine_router_port: Optional[int] = None, ) -> str: - client = get_llm_client(api_key=api_key, nine_router_port=nine_router_port) + env_dict: SDK_ENV_DICT = resolve_sdk_env(api_key=api_key, nine_router_port=nine_router_port) + client = anthropic.AsyncAnthropic( + api_key=env_dict["ANTHROPIC_API_KEY"], + base_url=env_dict.get("ANTHROPIC_BASE_URL"), + ) resp: AnthropicMessage = await client.messages.create( model=model, max_tokens=max_tokens, - system=system, - messages=[{"role": "user", "content": user_content}], + system=system_prompt, + messages=[{"role": "user", "content": user_prompt}], ) return resp.content[0].text.strip() \ No newline at end of file diff --git a/backend/core/llm/get_llm_client.py b/backend/core/llm/resolve_sdk_env.py similarity index 60% rename from backend/core/llm/get_llm_client.py rename to backend/core/llm/resolve_sdk_env.py index 48b093bc..821dce85 100644 --- a/backend/core/llm/get_llm_client.py +++ b/backend/core/llm/resolve_sdk_env.py @@ -1,7 +1,6 @@ -import anthropic import httpx from typeguard import typechecked -from typing import Optional +from typing import Optional, Literal, Dict from backend.core.generic_utils.assert_exactly_one_optional import assert_exactly_one_optional @typechecked @@ -12,28 +11,23 @@ def p_check_9router(nine_router_port: int) -> bool: except Exception: return False +SDK_ENV_DICT = Dict[Literal["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"], str] @typechecked -def get_llm_client( +def resolve_sdk_env( api_key: Optional[str] = None, nine_router_port: Optional[int] = None, -) -> anthropic.AsyncAnthropic: - """Build an AsyncAnthropic client. - - Priority: API key → 9Router subscription - """ +) -> SDK_ENV_DICT: + """Resolve credentials into an env dict for ClaudeAgentOptions.""" assert_exactly_one_optional([api_key, nine_router_port]) - - if api_key is not None: - return anthropic.AsyncAnthropic(api_key=api_key) - - elif nine_router_port is not None: + if api_key: + return {"ANTHROPIC_API_KEY": api_key} + elif nine_router_port: if not p_check_9router(nine_router_port): raise ValueError("9Router is not running. Set an API key or connect a subscription.") - return anthropic.AsyncAnthropic( - api_key="9router", - base_url=f"http://localhost:{nine_router_port}", - ) - + return { + "ANTHROPIC_API_KEY": "9router", + "ANTHROPIC_BASE_URL": f"http://localhost:{nine_router_port}", + } else: - raise ValueError("No AI provider configured. Set an API key or a 9Router port.") + raise ValueError("No AI provider configured. Set an API key or a 9router port") \ No newline at end of file diff --git a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py index a84032d5..1eda032e 100644 --- a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py +++ b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py @@ -34,6 +34,7 @@ async def run_browser_agent( tool_names.append(tool.to_sdk_args()) browser_config = ClaudeAgentOptions( + env=parent.config.env, model=parent.model, system_prompt=BROWSER_AGENT_SYSTEM_PROMPT, tools=tool_names,