96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""Frontend ABC + the runtime context handed to ``configure``.
|
|
|
|
A frontend is anything that listens on a port and routes inbound traffic
|
|
into the gateway. ``GatewayRuntime`` carries everything a frontend may
|
|
need that isn't user-config: built registries, per-agent backends, and
|
|
the in-memory token store. The user's ``/config/config.py`` defines a
|
|
``Gateway`` (lists); ``cli.main`` turns that into a ``GatewayRuntime``
|
|
and hands it to each frontend's ``configure``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
|
|
from beaver_gateway.backends.base import Backend
|
|
from beaver_gateway.core.auth import TokenStore
|
|
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
|
|
from beaver_gateway.core.turn_record import TurnRecord
|
|
from beaver_gateway.storage import Database
|
|
|
|
TurnLogHandler = Callable[[TurnRecord], Awaitable[None]]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GatewayRuntime:
|
|
"""Post-build state of the gateway, shared with every frontend.
|
|
|
|
Backends are keyed by **agent name**, not type — one ``RaycastBackend``
|
|
instance can serve many ``RaycastAgent`` instances, but the lookup
|
|
site (an inbound request with ``model=<agent.name>``) already has
|
|
the name in hand, so the indirection lives one step earlier.
|
|
|
|
``mcp_internal_urls`` is filled in Phase 2.1: one loopback URL per
|
|
declared ``McpServer`` so ``ClaudeSdkBackend``
|
|
can pass them to ``BackendOptions.mcp_servers`` without re-running
|
|
discovery.
|
|
|
|
``db`` (Phase 4.1) is the shared :class:`Database` handle. Phase 4.2
|
|
will switch ``TokenStore`` to read from it; Phase 4.3 admin/audit
|
|
write through it. Phase 4.1 only attaches it — existing frontends
|
|
ignore it.
|
|
"""
|
|
|
|
agents: AgentRegistry
|
|
mcps: McpRegistry
|
|
backends: dict[str, Backend]
|
|
token_store: TokenStore
|
|
db: Database
|
|
mcp_internal_urls: Mapping[str, str] = field(default_factory=dict)
|
|
# Phase 4.3 — AdminFrontend reads creds + cookie-signing key from
|
|
# the runtime so the user's ``config.py`` doesn't have to know
|
|
# anything about env wiring. Defaulted to empty so existing tests /
|
|
# call sites that don't touch the admin path keep building; the
|
|
# admin frontend ``configure()`` itself rejects empty values.
|
|
admin_user: str = ""
|
|
admin_pass: str = ""
|
|
session_secret: str = ""
|
|
# The full sibling-frontends list, in declaration order. AdminFrontend
|
|
# uses it to advertise concrete bearer-endpoint URLs (host/port) on
|
|
# the dashboard so the operator can copy ready-to-use links / curl
|
|
# snippets. Other frontends ignore it.
|
|
frontends: Sequence[Frontend] = field(default_factory=tuple)
|
|
# Frontends that finish a turn (Anthropic Messages, Markdown) iterate
|
|
# this list and ``await`` each handler with a ``TurnRecord``. Handlers
|
|
# are appended during ``configure()`` by frontends that want a
|
|
# cross-frontend chat archive — currently the markdown frontend's
|
|
# ``log_all_chats`` mode. Handler exceptions are caught at the call
|
|
# site; they never block the user-visible response.
|
|
#
|
|
# The field is typed as ``list[Any]`` rather than the precise
|
|
# ``list[TurnLogHandler]`` because the alias lives under TYPE_CHECKING
|
|
# to keep ``anthropic.types`` out of the runtime import graph for
|
|
# this base module.
|
|
turn_log_handlers: list[TurnLogHandler] = field(default_factory=list)
|
|
# M1b: conversations service, event bus and the shared session pool.
|
|
# ``Any`` for the same import-graph reason as above; ``None`` only in
|
|
# tests that build a runtime without them.
|
|
conversations: Any = None
|
|
bus: Any = None
|
|
pool: Any = None
|
|
|
|
|
|
class Frontend(ABC):
|
|
"""Listens on a port, dispatches into the gateway."""
|
|
|
|
@abstractmethod
|
|
def configure(self, runtime: GatewayRuntime) -> None: ...
|
|
|
|
@abstractmethod
|
|
async def serve(self) -> None: ...
|