123 lines
5.4 KiB
Markdown
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.
|
|
|
|

|
|
|
|
## 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.
|
|
|
|

|
|
|
|
 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.
|