"""Butler Orchestrator — bridges Butler product layer with the Agent Loop.""" from __future__ import annotations import logging import threading from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from butler.core.agent_loop.loop import AgentLoop from butler.transport.llm_client import LLMClient from butler.configuration.settings import get_butler_settings from butler.memory import ButlerMemory, ProjectMemory from butler.memory.facade import ButlerMemoryService # test patch target from butler.orchestrator import loop_factory, memory_bridge, prompt_assembler, skill_bridge from butler.orchestrator.templates import normalize_butler_role from butler.project.manager import get_project_manager from butler.skills.router import SkillRouter import butler.workflows # noqa: F401 — register workflow hooks logger = logging.getLogger(__name__) class ButlerOrchestrator: """Bridge Butler configuration/memories into AgentLoop instances.""" def __init__(self, user_id: str = "owner", channel: str = "cli") -> None: self.user_id = user_id self.channel = channel self._settings = get_butler_settings() self._memory_by_tenant: dict[str, ButlerMemory] = {} self._memory_lock = threading.Lock() self.project_manager = get_project_manager() self._project_memory: ProjectMemory | None = None self._skill_router: SkillRouter | None = None self._skill_manager = None self.memory_provider = None self.memory_offline: bool = False self._memory_init_error: str = "" memory_bridge.reload_project_memory(self) skill_bridge.rebuild_skill_router(self) memory_bridge.initialize_memory_provider(self) self.project_manager.on_switch(self._pm_switch) def _pm_switch(self, old_project: str, new_project: str) -> None: self.on_project_switch(old_project, new_project) @property def butler_memory(self) -> ButlerMemory: return memory_bridge.get_butler_memory(self) def _model_credentials(self, role: str) -> dict[str, Any]: from butler.model_resolve import model_config_to_credentials, resolve_effective_model role = normalize_butler_role(role) project = self.project_manager.get_current() em = resolve_effective_model(role, project=project, settings=self._settings) return cast(dict[str, Any], model_config_to_credentials(em.config, settings=self._settings)) def build_memory_context(self, *, for_role: str = "default") -> str: return cast(str, memory_bridge.build_memory_context(self, for_role=for_role)) def _system_prompt_placeholders(self, *, for_role: str = "default") -> dict[str, str]: return cast( dict[str, str], prompt_assembler.system_prompt_placeholders(self, for_role=for_role), ) def build_dynamic_system_reminder(self, *, for_role: str = "default") -> str: return cast( str, prompt_assembler.build_dynamic_system_reminder(self, for_role=for_role), ) def build_static_system_prompt(self) -> str: return cast(str, prompt_assembler.build_static_system_prompt(self)) def resolve_system_prompt( self, *, role: str = "butler", session_key: str = "", ) -> tuple[str, str | None]: return cast( tuple[str, str | None], prompt_assembler.resolve_system_prompt( self, role=role, session_key=session_key ), ) def _assemble_default_system_prompt(self, *, for_role: str = "default") -> str: return cast( str, prompt_assembler.assemble_default_system_prompt(self, for_role=for_role), ) def build_system_prompt(self) -> str: return cast(str, prompt_assembler.build_system_prompt(self)) def build_lead_system_prompt(self, *, session_key: str = "") -> str: return cast( str, prompt_assembler.build_lead_system_prompt(self, session_key=session_key), ) def get_agent_kwargs(self) -> dict[str, Any]: return cast(dict[str, Any], loop_factory.get_agent_kwargs(self)) def create_llm_client(self, role: str = "butler") -> LLMClient: return loop_factory.create_llm_client(self, role) def create_agent_loop( self, role: str = "butler", *, tools: list[dict[str, Any]] | None = None, tool_dispatcher: Any = None, callbacks: Any = None, session_key: str = "", ) -> AgentLoop: return loop_factory.create_agent_loop( self, role, tools=tools, tool_dispatcher=tool_dispatcher, callbacks=callbacks, session_key=session_key, ) def create_project_agent_loop( self, role: str = "dev", *, tools: list[dict[str, Any]] | None = None, tool_dispatcher: Any = None, callbacks: Any = None, session_key: str = "", ) -> AgentLoop: return loop_factory.create_project_agent_loop( self, role, tools=tools, tool_dispatcher=tool_dispatcher, callbacks=callbacks, session_key=session_key, ) def get_project_agent_kwargs(self, role: str) -> dict[str, Any]: return cast(dict[str, Any], loop_factory.get_project_agent_kwargs(self, role)) def on_project_switch(self, old_project: str, new_project: str) -> None: logger.debug( "Butler project switched: %r -> %r", old_project, new_project, ) memory_bridge.reload_project_memory(self) skill_bridge.rebuild_skill_router(self) memory_bridge.refresh_memory_provider_for_project_switch(self) def _reload_project_memory(self) -> None: memory_bridge.reload_project_memory(self) def _rebuild_skill_router(self) -> None: skill_bridge.rebuild_skill_router(self) def _initialize_memory_provider(self) -> None: memory_bridge.initialize_memory_provider(self) def _refresh_memory_provider_for_project_switch(self) -> None: memory_bridge.refresh_memory_provider_for_project_switch(self) def _build_skill_injection_sections( self, matched: list[dict[str, Any]], *, header_note: str, ) -> list[str]: return cast( list[str], skill_bridge.build_skill_injection_sections( matched, header_note=header_note ), ) def inject_skill_context( self, task_description: str, top_k: int = 3, *, diagnostics: dict[str, Any] | None = None, ) -> str: return cast( str, skill_bridge.inject_skill_context( self, task_description, top_k=top_k, diagnostics=diagnostics, ), ) __all__ = [ "ButlerOrchestrator", "ButlerMemoryService", "build_dynamic_system_reminder", "build_lead_system_prompt", "build_memory_context", "build_static_system_prompt", "build_system_prompt", "butler_memory", "create_agent_loop", "create_llm_client", "create_project_agent_loop", "get_agent_kwargs", "get_project_agent_kwargs", "inject_skill_context", "on_project_switch", "resolve_system_prompt", ]