118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""Agent / MCP registries + the user-facing ``Gateway`` collector.
|
|
|
|
The user's ``/config/config.py`` ends with::
|
|
|
|
gateway = Gateway(agents=[...], mcps=[...], frontends=[...])
|
|
|
|
``cli.main`` picks that object up and builds the registries.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable, Iterable, Iterator
|
|
|
|
from beaver_gateway.agents.base import BaseAgent
|
|
from beaver_gateway.core.conversations import ConversationTexts, UserSaid
|
|
from beaver_gateway.core.distill import Distiller
|
|
from beaver_gateway.core.envelope import RecallContext
|
|
from beaver_gateway.core.rotation import RotationPolicy
|
|
from beaver_gateway.core.scheduler import Budget, Job
|
|
from beaver_gateway.core.watch import VaultWatch
|
|
from beaver_gateway.frontends.base import Frontend
|
|
from beaver_gateway.mcp.types import McpServerT
|
|
|
|
|
|
class AgentRegistry:
|
|
"""Name → agent lookup with duplicate detection."""
|
|
|
|
def __init__(self, agents: Iterable[BaseAgent]) -> None:
|
|
self._agents: dict[str, BaseAgent] = {}
|
|
for a in agents:
|
|
if a.name in self._agents:
|
|
msg = f"duplicate agent name: {a.name!r}"
|
|
raise ValueError(msg)
|
|
self._agents[a.name] = a
|
|
|
|
def __getitem__(self, name: str) -> BaseAgent:
|
|
return self._agents[name]
|
|
|
|
def get(self, name: str) -> BaseAgent | None:
|
|
return self._agents.get(name)
|
|
|
|
def __iter__(self) -> Iterator[BaseAgent]:
|
|
return iter(self._agents.values())
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._agents)
|
|
|
|
def __contains__(self, name: object) -> bool:
|
|
return name in self._agents
|
|
|
|
|
|
class McpRegistry:
|
|
"""Name → MCP server lookup with duplicate detection."""
|
|
|
|
def __init__(self, mcps: Iterable[McpServerT]) -> None:
|
|
self._mcps: dict[str, McpServerT] = {}
|
|
for m in mcps:
|
|
if m.name in self._mcps:
|
|
msg = f"duplicate mcp name: {m.name!r}"
|
|
raise ValueError(msg)
|
|
self._mcps[m.name] = m
|
|
|
|
def __getitem__(self, name: str) -> McpServerT:
|
|
return self._mcps[name]
|
|
|
|
def get(self, name: str) -> McpServerT | None:
|
|
return self._mcps.get(name)
|
|
|
|
def __iter__(self) -> Iterator[McpServerT]:
|
|
return iter(self._mcps.values())
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._mcps)
|
|
|
|
def __contains__(self, name: object) -> bool:
|
|
return name in self._mcps
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Gateway:
|
|
"""Top-level object the user assembles in ``/config/config.py``."""
|
|
|
|
agents: list[BaseAgent] = field(default_factory=list)
|
|
mcps: list[McpServerT] = field(default_factory=list)
|
|
frontends: list[Frontend] = field(default_factory=list)
|
|
texts: ConversationTexts | None = None
|
|
"""Merge prompt and seed bodies for ``core/conversations`` (§8.2-8.3)."""
|
|
jobs: list[Job] = field(default_factory=list)
|
|
"""Cron / webhook / event jobs for ``core/scheduler`` (§3.6, §4.5)."""
|
|
rotation: RotationPolicy | None = None
|
|
"""When a master is rotated (§4.5); ``None`` keeps the defaults."""
|
|
watch: VaultWatch | None = None
|
|
"""Vault watcher feeding the envelope (§3.5, §4.6); ``None`` = no vault block."""
|
|
recall: Callable[[RecallContext], str | None] | None = None
|
|
"""Envelope lookup on the user's text: pointers into the vault (cards,
|
|
the agent's notes, due dates) the gateway knows no paths for (§3.3)."""
|
|
user_sink: Callable[[UserSaid], Awaitable[None] | None] | None = None
|
|
"""Sees every user message as it enters a master or branch turn - the
|
|
setup's own grep-able log of what the user said, outside the transcript."""
|
|
budget: Budget | None = None
|
|
"""Subscription window past which non-critical jobs wait (§4.5)."""
|
|
distiller: Distiller | None = None
|
|
"""Who closes deep chats and where the digests and the index live (§8.4)."""
|
|
tz: str = "UTC"
|
|
"""Local zone for the envelope clock and the rotation hour."""
|
|
host: str = "0.0.0.0" # noqa: S104
|
|
port: int = 8000
|
|
"""The one listener; every HTTP frontend is mounted under its ``path``."""
|
|
public_url: str | None = None
|
|
"""Origin the reverse proxy shows the world (``https://b.example.com``).
|
|
|
|
Advertised endpoints and MCP discovery are built on it; ``None``
|
|
derives the origin from each request."""
|