107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""Frontend ABC and the runtime context handed to ``configure``.
|
|
|
|
A frontend routes inbound traffic into the gateway (an HTTP mount, a
|
|
poller, or both); ``GatewayRuntime`` carries the built state each needs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from beaver_gateway.security.auth import BUILTIN_SCOPES
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
|
|
from starlette.types import ASGIApp
|
|
|
|
from beaver_gateway.app import AgentRegistry, McpRegistry
|
|
from beaver_gateway.backends.base import Backend
|
|
from beaver_gateway.conversations.kinds import Kind
|
|
from beaver_gateway.frontends.turn_record import TurnRecord
|
|
from beaver_gateway.security.auth import TokenStore
|
|
from beaver_gateway.storage import Database
|
|
from beaver_gateway.storage.models import Conversation, ConversationBinding
|
|
|
|
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 backend instance may
|
|
serve several agents, so the indirection lives at lookup time.
|
|
"""
|
|
|
|
agents: AgentRegistry
|
|
mcps: McpRegistry
|
|
backends: dict[str, Backend]
|
|
token_store: TokenStore
|
|
db: Database
|
|
mcp_internal_urls: Mapping[str, str] = field(default_factory=dict)
|
|
admin_user: str = ""
|
|
"""Operator login for the admin console, checked by ``AdminFrontend``."""
|
|
admin_pass: str = ""
|
|
session_secret: str = ""
|
|
frontends: Sequence[Frontend] = field(default_factory=tuple)
|
|
"""Every frontend in declaration order, for advertising their URLs."""
|
|
turn_log_handlers: list[TurnLogHandler] = field(default_factory=list)
|
|
"""Called with a ``TurnRecord`` after each turn; failures never reach the user."""
|
|
conversations: Any = None
|
|
bus: Any = None
|
|
pool: Any = None
|
|
scheduler: Any = None
|
|
public_url: str | None = None
|
|
"""``Gateway.public_url``; ``None`` derives the origin from the request."""
|
|
scopes: frozenset[str] = BUILTIN_SCOPES
|
|
"""Every token scope this gateway knows: the builtins plus each frontend's."""
|
|
|
|
|
|
class Frontend(ABC):
|
|
"""Routes inbound traffic into the gateway.
|
|
|
|
HTTP frontends set ``path`` and return their ASGI app from ``app()``;
|
|
these are mounted under that path on the one gateway port. ``serve()``
|
|
is for non-HTTP work (polling, vault mirrors) and defaults to nothing.
|
|
``landing`` marks the app that ``/`` redirects to.
|
|
|
|
``scope`` is the token scope this frontend's routes are gated by; a
|
|
frontend that names its own scope gets keys nobody else's token opens,
|
|
and ``POST /api/tokens`` will mint them (see ``security/auth.py``).
|
|
|
|
A frontend that shows conversations declares ``name`` (the binding
|
|
key) and ``kinds`` (which conversation kinds it shows). The first
|
|
frontend whose ``materialize`` returns a binding is the *home* of
|
|
that kind, used by ``spawn`` for new conversations; ``agent_for``
|
|
names the default agent for a kind. Stateless frontends (MCP, admin)
|
|
leave these at their defaults.
|
|
"""
|
|
|
|
name: str = ""
|
|
kinds: tuple[Kind, ...] = ()
|
|
path: str | None = None
|
|
landing: bool = False
|
|
scope: str | None = None
|
|
|
|
@abstractmethod
|
|
def configure(self, runtime: GatewayRuntime) -> None: ...
|
|
|
|
def app(self) -> ASGIApp | None:
|
|
return None
|
|
|
|
async def serve(self) -> None:
|
|
return
|
|
|
|
def agent_for(self, kind: Kind) -> str | None: # noqa: ARG002
|
|
return None
|
|
|
|
async def materialize(self, conv: Conversation) -> ConversationBinding | None: # noqa: ARG002
|
|
return None
|
|
|
|
async def mark_closed(self, conv: Conversation) -> bool: # noqa: ARG002
|
|
"""Show in the window that the conversation is over (a renamed topic)."""
|
|
return False
|