refactor: add markdown frontend
This commit is contained in:
@@ -45,6 +45,8 @@ from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
from beaver_gateway.core import audit
|
||||
from beaver_gateway.core.auth import VALID_SCOPES, hash_token
|
||||
from beaver_gateway.core.turn_record import TurnRecord
|
||||
from beaver_gateway.frontends._accumulate import StreamAccumulator
|
||||
from beaver_gateway.frontends.base import Frontend
|
||||
from beaver_gateway.storage import (
|
||||
create_token,
|
||||
@@ -57,6 +59,8 @@ if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import datetime
|
||||
|
||||
from anthropic.types import MessageParam
|
||||
|
||||
from beaver_gateway.core.events import MessageStreamEvent
|
||||
from beaver_gateway.frontends.base import GatewayRuntime
|
||||
|
||||
@@ -449,7 +453,15 @@ class AdminFrontend(Frontend):
|
||||
system=system if isinstance(system, str) else None,
|
||||
)
|
||||
return StreamingResponse(
|
||||
_sse_events(events), media_type="text/event-stream"
|
||||
_sse_events_and_broadcast(
|
||||
events,
|
||||
runtime=runtime,
|
||||
agent_name=agent.name,
|
||||
input_messages=messages,
|
||||
system=system if isinstance(system, str) else None,
|
||||
model=agent.model or agent.name,
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
return app
|
||||
@@ -510,23 +522,31 @@ def _build_endpoint_catalog(
|
||||
and may have non-trivial transitive deps (aiohttp etc.).
|
||||
"""
|
||||
from beaver_gateway.frontends.anthropic import AnthropicMessagesFrontend
|
||||
from beaver_gateway.frontends.markdown import MarkdownFrontend
|
||||
from beaver_gateway.frontends.mcp_server import McpServerFrontend
|
||||
|
||||
scheme = request.headers.get("x-forwarded-proto") or request.url.scheme
|
||||
hostname = request.url.hostname or "localhost"
|
||||
|
||||
def _base_for(fe: AnthropicMessagesFrontend | McpServerFrontend) -> str:
|
||||
def _base_for(
|
||||
fe: AnthropicMessagesFrontend | McpServerFrontend | MarkdownFrontend,
|
||||
) -> str:
|
||||
if fe.public_base_url:
|
||||
return fe.public_base_url
|
||||
return f"{scheme}://{hostname}:{fe.port}"
|
||||
|
||||
anthropic_base: str | None = None
|
||||
mcp_base: str | None = None
|
||||
markdown_fe: MarkdownFrontend | None = None
|
||||
markdown_base: str | None = None
|
||||
for fe in runtime.frontends:
|
||||
if isinstance(fe, AnthropicMessagesFrontend) and anthropic_base is None:
|
||||
anthropic_base = _base_for(fe)
|
||||
elif isinstance(fe, McpServerFrontend) and mcp_base is None:
|
||||
mcp_base = _base_for(fe)
|
||||
elif isinstance(fe, MarkdownFrontend) and markdown_fe is None:
|
||||
markdown_fe = fe
|
||||
markdown_base = _base_for(fe)
|
||||
|
||||
agent_rows: list[dict[str, Any]] = []
|
||||
if anthropic_base is not None:
|
||||
@@ -554,27 +574,57 @@ def _build_endpoint_catalog(
|
||||
{"namespace": "all", "kind": "bundle", "url": f"{mcp_base}/all/"}
|
||||
)
|
||||
|
||||
markdown_row: dict[str, Any] | None = None
|
||||
if markdown_fe is not None and markdown_base is not None:
|
||||
# Pick a sane example filename from the vault, prefer something
|
||||
# the user can recognise. Falls back to a generic ``chat.md``.
|
||||
sample_agent = markdown_fe.default_agent or next(
|
||||
(a.name for a in runtime.agents), "<agent>"
|
||||
)
|
||||
markdown_row = {
|
||||
"url": f"{markdown_base}/chat",
|
||||
"vault_path": str(markdown_fe.vault_path),
|
||||
"default_agent": markdown_fe.default_agent,
|
||||
"log_all_chats": markdown_fe.log_all_chats,
|
||||
"logged_subdir": markdown_fe.logged_subdir,
|
||||
"sample_agent": sample_agent,
|
||||
}
|
||||
|
||||
return {
|
||||
"anthropic_base": anthropic_base,
|
||||
"mcp_base": mcp_base,
|
||||
"markdown_base": markdown_base,
|
||||
"agents": agent_rows,
|
||||
"mcps": mcp_rows,
|
||||
"markdown": markdown_row,
|
||||
}
|
||||
|
||||
|
||||
async def _sse_events(
|
||||
async def _sse_events_and_broadcast(
|
||||
events: AsyncIterator[MessageStreamEvent],
|
||||
*,
|
||||
runtime: GatewayRuntime,
|
||||
agent_name: str,
|
||||
input_messages: list[MessageParam],
|
||||
system: str | None,
|
||||
model: str,
|
||||
) -> AsyncIterator[bytes]:
|
||||
r"""Serialize a backend stream into Anthropic's ``text/event-stream`` form.
|
||||
r"""Serialize a backend stream as SSE and broadcast a ``TurnRecord`` after.
|
||||
|
||||
Same wire shape as :mod:`beaver_gateway.frontends.anthropic` —
|
||||
duplicated rather than imported so the admin frontend stays
|
||||
independent of that module's private helpers, and so a mid-stream
|
||||
failure surfaces as an in-band ``error`` event the chat UI can
|
||||
render rather than a dangling connection.
|
||||
render rather than a dangling connection. The events also feed a
|
||||
side ``StreamAccumulator`` so once the SSE response closes we hand
|
||||
the assembled ``Message`` to ``runtime.turn_log_handlers`` (the
|
||||
markdown frontend's archive logger lives there). ``source="admin"``
|
||||
so the cross-frontend logger knows where the turn came from.
|
||||
"""
|
||||
acc = StreamAccumulator()
|
||||
try:
|
||||
async for ev in events:
|
||||
acc.feed(ev)
|
||||
payload = ev.model_dump_json()
|
||||
yield f"event: {ev.type}\ndata: {payload}\n\n".encode()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -583,6 +633,22 @@ async def _sse_events(
|
||||
{"type": "error", "error": {"type": "api_error", "message": str(exc)}}
|
||||
)
|
||||
yield f"event: error\ndata: {err}\n\n".encode()
|
||||
return
|
||||
if not runtime.turn_log_handlers:
|
||||
return
|
||||
message = acc.finalize(model=model)
|
||||
record = TurnRecord(
|
||||
agent_name=agent_name,
|
||||
input_messages=list(input_messages),
|
||||
output_message=message,
|
||||
system=system,
|
||||
source="admin",
|
||||
)
|
||||
for handler in runtime.turn_log_handlers:
|
||||
try:
|
||||
await handler(record)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception("turn_log_handler raised; continuing")
|
||||
|
||||
|
||||
def _set_session_cookie(response: Response, value: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user