Files
beaver-gateway/README.md
T

123 lines
5.4 KiB
Markdown

# beaver-gateway
A gateway for personal agents built on the Claude Agent SDK: conversations
with a lifecycle, windows to talk through, jobs, memory hooks, and a policy
boundary - without an opinion about what the agent is for. A setup is one
`config.py` (or a package next to it) that assembles a `Gateway`; this repo
holds the primitives, [beaver-agent](https://git.kotikot.com/beaver/beaver-agent)
is a full setup built on them.
![The admin: what is in motion, what waits for an answer, the context and the quotas, the graph around the master](preview/shots/admin-now.webp)
## What it gives a setup
- **agents/** - what an agent is: a Claude agent with a prompt per
conversation kind, skill sets per kind, gateway tools, a `PreToolUse`
policy; a Raycast agent for cheap fast answers.
- **backends/** - how a turn runs: the Claude Agent SDK subprocess with a
session pool, transcripts and session stores; the Raycast wire.
- **conversations/** - the model of a conversation: kinds (master, branch,
deep, job, fork), rows and windows, the inject queue with priorities,
seeds, turns, questions, closing through a distiller, master rotation,
the envelope, the in-process gateway tools.
- **jobs/** - cron, webhook and event jobs on pgqueuer, deferred injects,
the subscription budget.
- **frontends/** - the windows: Telegram (master = General, topic = branch,
files out of the agent through `send_file`),
markdown files in a vault, `/api` + the admin SPA, an Anthropic-compatible
`/anthropic/v1/messages`, MCP re-exposure at `/mcp/<name>`, and
`WebhookFrontend` for a window a setup declares itself - its own request
and response schema, its own agent, its own token scope
(`docs/FRONTEND-PLUGINS.md`).
- **mcp/** - MCP servers a setup declares (stdio, http, python tools),
aggregated in-process and handed to agents by name.
- **vault/** - watching a directory of notes for the envelope.
- **security/** - bearer tokens, the audit log, credential redaction.
- **keystore/** - secrets the model runs commands with: values and files
sealed with `SECRETS_KEY` (AES-256-GCM) in the gateway's database, set
write-only from the admin, handed out only through `secret run NAME --
cmd` (one `sudo` rule for `beaver-runner`, a root-only socket, files in
`/dev/shm`, values masked in the command's output).
- **transcribe/** - speech to text for the voice notes, round videos and
audio a window receives: providers tried in order, `OpenAITranscriber`
for any OpenAI-compatible endpoint built in.
- **events/** - the in-process bus every frontend taps, and the stream
event protocol backends emit.
- **storage/** - SQLModel tables and the Postgres session store.
`config.py` defines `Gateway` and loads a setup; `app.py` builds the runtime
from it; `cli.py` is the entrypoint. The admin SPA in `ui/` is the browser
window on all of it: conversations by day, the thread with the tree of tool
calls, the context of the last turn, memory, jobs, usage, tokens, secrets and the audit.
![A conversation in the admin: the thread, the queue, the details](preview/shots/admin-thread.webp)
![The Context tab: what the model held on the last turn - harness, prompt granules, skills, the notes the thread reached](preview/shots/admin-context.webp) Every string the gateway puts in front of
a model or a user has an English default and is overridable
(`ConversationTexts`, `TelegramTexts`).
## Run
```sh
cd examples
CLAUDE_CODE_OAUTH_TOKEN=$(claude setup-token) docker compose up --build
```
`examples/config.py` is the smallest setup that starts: one agent, one
python tool, one cron job, the built-in frontends on port 8000; the compose
file next to it adds Postgres. Without Docker: `uv sync`, build the SPA with
`cd ui && bun install && bun run build`, then run `uv run beaver-gateway` with
`DATABASE_URL` and `CONFIG_PATH` set. A setup's own secrets come from `.env`
next to its config (`load_dotenv` runs before the config is executed).
## Transcription
A voice note, a round video or an audio file sent to the Telegram window is
downloaded as any attachment; with `Gateway.transcribers` set, the frontend
runs them in order and the first non-empty text rides under the attachment
note (`TelegramTexts.attachment_transcript`). A provider that raises is
logged and the next one tried; the whole chain gives up after the
frontend's `transcribe_timeout` (90 s) and the note goes without the text.
No transcribers, nothing changes.
```python
import os
from beaver_gateway.transcribe import Audio, OpenAITranscriber
async def voice_only(audio: Audio) -> str | None:
if audio.media_type != "audio/ogg":
return None
return await my_service.transcribe(audio.path, seconds=audio.duration)
gateway = Gateway(
transcribers=[
voice_only,
OpenAITranscriber(
base_url="https://api.groq.com/openai/v1",
api_key=os.environ["GROQ_API_KEY"],
model="whisper-large-v3",
language="ru",
),
],
)
```
A provider is any `async (Audio) -> str | None`; `None` passes the audio
on. `OpenAITranscriber` posts the file to `{base_url}/audio/transcriptions`,
the wire of OpenAI, Groq, faster-whisper-server and a whisper.cpp server;
`api_key=None` for a local one without auth.
## Develop
```sh
make check # ruff format --check, ruff check, ty, pytest
make fix
```
No comments in code: modules carry a one-line docstring saying what they are
for, public configuration fields carry a one-line docstring saying what they
mean; everything else is the code.