feat: implement raycast backend
This commit is contained in:
+136
-9
@@ -1,28 +1,155 @@
|
||||
"""Process entrypoint.
|
||||
|
||||
Phase 0.3 — load the user's ``/config/config.py`` via
|
||||
``config_loader``, build registries, print the
|
||||
``loaded N agents, M mcps, K frontends`` line from the Phase 0 DoD,
|
||||
exit cleanly. Phase 0.4 will install uvloop and start uvicorn(s) for
|
||||
the frontends and the internal MCP app.
|
||||
Phase 1.4 — async ``main``: install uvloop, load the user config, build
|
||||
registries + per-agent backends (only ``RaycastBackend`` so far), wire
|
||||
each frontend with a ``GatewayRuntime``, and run all
|
||||
``frontend.serve()`` coroutines concurrently. Without any frontends we
|
||||
still print the Phase 0 DoD line and exit cleanly so the bare skeleton
|
||||
keeps working.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import uvloop
|
||||
from raycast_api import Client as RaycastClient
|
||||
from raycast_api.config import Config as RaycastConfig
|
||||
|
||||
from beaver_gateway import config_loader
|
||||
from beaver_gateway.agents.raycast import RaycastAgent
|
||||
from beaver_gateway.backends.raycast import RaycastBackend
|
||||
from beaver_gateway.core.auth import TokenStore
|
||||
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
|
||||
from beaver_gateway.frontends.base import GatewayRuntime
|
||||
from beaver_gateway.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver_gateway.backends.base import Backend
|
||||
|
||||
|
||||
_log = logging.getLogger("beaver_gateway.cli")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = Settings() # ty: ignore[missing-argument]
|
||||
"""Sync wrapper: uvloop loop factory + asyncio.run."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
asyncio.run(_async_main(), loop_factory=uvloop.new_event_loop)
|
||||
|
||||
|
||||
async def _async_main() -> None:
|
||||
settings = Settings() # ty: ignore[missing-argument]
|
||||
gateway = config_loader.load(settings.config_path)
|
||||
|
||||
agents = AgentRegistry(gateway.agents)
|
||||
mcps = McpRegistry(gateway.mcps)
|
||||
token_store = TokenStore.from_env(settings.bootstrap_tokens)
|
||||
|
||||
print(
|
||||
f"beaver-gateway: loaded {len(agents)} agents, "
|
||||
f"{len(mcps)} mcps, {len(gateway.frontends)} frontends"
|
||||
async with AsyncExitStack() as stack:
|
||||
backends: dict[str, Backend] = await _build_backends(
|
||||
settings=settings, agents=agents, stack=stack
|
||||
)
|
||||
|
||||
runtime = GatewayRuntime(
|
||||
agents=agents,
|
||||
mcps=mcps,
|
||||
backends=backends,
|
||||
token_store=token_store,
|
||||
)
|
||||
|
||||
for fe in gateway.frontends:
|
||||
fe.configure(runtime)
|
||||
|
||||
_log.info(
|
||||
"beaver-gateway: loaded %d agents, %d mcps, %d frontends",
|
||||
len(agents),
|
||||
len(mcps),
|
||||
len(gateway.frontends),
|
||||
)
|
||||
|
||||
# Keep the Phase 0 DoD line on stdout for grep-friendly smoke
|
||||
# tests, in addition to the structured log line above.
|
||||
print(
|
||||
f"beaver-gateway: loaded {len(agents)} agents, "
|
||||
f"{len(mcps)} mcps, {len(gateway.frontends)} frontends"
|
||||
)
|
||||
|
||||
if not gateway.frontends:
|
||||
return
|
||||
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for fe in gateway.frontends:
|
||||
tg.create_task(fe.serve())
|
||||
|
||||
|
||||
async def _build_backends(
|
||||
*,
|
||||
settings: Settings,
|
||||
agents: AgentRegistry,
|
||||
stack: AsyncExitStack,
|
||||
) -> dict[str, Backend]:
|
||||
"""Construct one backend per agent name.
|
||||
|
||||
The Raycast ``Client`` is shared across every ``RaycastAgent``
|
||||
(bearer + device-id are process-wide), so we open it lazily — only
|
||||
when at least one ``RaycastAgent`` is present — and close it via
|
||||
the caller's exit stack.
|
||||
"""
|
||||
backends: dict[str, Backend] = {}
|
||||
|
||||
raycast_agents = [a for a in agents if isinstance(a, RaycastAgent)]
|
||||
if raycast_agents:
|
||||
client = await _try_open_raycast_client(settings, stack)
|
||||
if client is not None:
|
||||
raycast_backend = RaycastBackend(client)
|
||||
for a in raycast_agents:
|
||||
backends[a.name] = raycast_backend
|
||||
|
||||
# Phase 2: ClaudeAgent → ClaudeCodeBackendAdapter goes here.
|
||||
|
||||
return backends
|
||||
|
||||
|
||||
async def _try_open_raycast_client(
|
||||
settings: Settings, stack: AsyncExitStack
|
||||
) -> RaycastClient | None:
|
||||
"""Open a ``raycast_api.Client`` if creds are available, else warn + skip.
|
||||
|
||||
Skipping is gentler than failing startup: the user can bring up the
|
||||
gateway, list their agents, and still hit a ``ClaudeAgent``; affected
|
||||
``RaycastAgent`` instances 503 with a specific message at request time.
|
||||
Missing ``raycast.json`` falls into the same bucket — first-run
|
||||
users won't have it yet.
|
||||
"""
|
||||
if not settings.raycast_bearer:
|
||||
_log.warning(
|
||||
"RaycastAgent present but RAYCAST_BEARER is unset — those agents will 503"
|
||||
)
|
||||
return None
|
||||
if not settings.raycast_device_id:
|
||||
_log.warning(
|
||||
"RaycastAgent present but RAYCAST_DEVICE_ID is unset — those agents "
|
||||
"will 503 (generate once with `python -c 'import secrets; "
|
||||
"print(secrets.token_hex(32))'`)"
|
||||
)
|
||||
return None
|
||||
if not settings.raycast_config_path.exists():
|
||||
_log.warning(
|
||||
"RaycastAgent present but %s is missing — those agents will 503",
|
||||
settings.raycast_config_path,
|
||||
)
|
||||
return None
|
||||
|
||||
config = RaycastConfig.load(settings.raycast_config_path)
|
||||
client = RaycastClient(
|
||||
config=config,
|
||||
bearer_token=settings.raycast_bearer,
|
||||
device_id=settings.raycast_device_id,
|
||||
locale=settings.raycast_locale,
|
||||
)
|
||||
return await stack.enter_async_context(client)
|
||||
|
||||
Reference in New Issue
Block a user