79 lines
4.6 KiB
Markdown
79 lines
4.6 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Run the server (reads .env for config)
|
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
|
|
# Verify code compiles and imports cleanly
|
|
python -c "import main; print('OK')"
|
|
|
|
# Syntax check a specific file
|
|
python -m py_compile path/to/file.py
|
|
|
|
# Docker build
|
|
docker build -t agents-api -f docker/Dockerfile .
|
|
```
|
|
|
|
There is no test suite or linting setup yet.
|
|
|
|
## Architecture
|
|
|
|
This is a **Discord bot** powered by a LangGraph agent with a pluggable skill system. The only interaction surface is Discord DMs — there is no web chat UI.
|
|
|
|
```
|
|
Discord DM → bot.py → LangGraph StateGraph → skills (tools) → external APIs
|
|
│
|
|
REST API (auth status, JellyStat)
|
|
```
|
|
|
|
### Agent / skill system (`agents/`)
|
|
|
|
- **Agents** (`agents/__init__.py`) are thin wrappers pairing a system prompt with a list of skill names.
|
|
- **Skills** (`agents/skills/__init__.py`) provide prompt fragments, OpenAI tool definitions, and an async `execute` callback. Each skill self-registers via `register()` at import time.
|
|
- Agents and skills are loaded at startup by `load_all_agents()` in `main.py`, which triggers side-effecting imports of all agent/skill modules.
|
|
|
|
The media-agent (`agents/media_agent.py`) is the primary agent. Skills attached:
|
|
- `seerr` — search, trending, discover, request, submit issues via Seerr API
|
|
- `watch_history` — Jellyfin watch stats via the internal JellyStat API
|
|
- `media_info` — base persona (prompt-only)
|
|
- `triage` — fallback rules for unsupported actions (prompt-only)
|
|
- `easter_eggs` — theme-aware persona flavours (prompt-only)
|
|
|
|
### LangGraph graph (`src/graph.py`)
|
|
|
|
Two-node StateGraph: `agent_node → tool_node → agent_node`. The agent node calls DeepSeek (OpenAI-compatible) with system prompt + tool defs. The tool node executes tool calls through the skill system via `execute_tool()`. A custom tool node is used — not LangChain's ToolNode.
|
|
|
|
State is `AgentState` (`src/state.py`): a TypedDict with `messages` (LangGraph `add_messages` reducer) and `discord_user_id`.
|
|
|
|
### Discord bot (`gateway/discord/bot.py`)
|
|
|
|
Runs in a background daemon thread with its own asyncio event loop (separate from FastAPI's). DMs only — no server/channel messages. Requires users to share a guild with the bot. Maintains per-user conversation history via `ConversationStore` (in-memory dict, last N exchanges). Supports `/login <service>` (Quick Connect) and `/logout <service>`.
|
|
|
|
### Auth system (`src/auth_store.py`, `gateway/auth/`)
|
|
|
|
SQLite-backed (WAL mode) with a single `user_auth` table keyed on `(discord_user_id, service)`. Stores opaque service tokens, never passwords. `AuthService` is a minimal ABC with `name`/`display_name` properties; `JellyfinAuth` is the only implementation, using Jellyfin Quick Connect (initiate → poll → exchange secret for token). The auth gate in `execute_tool()` checks `skill.requires_auth` before executing tools.
|
|
|
|
### REST API (`main.py`)
|
|
|
|
Minimal — only two routers remain:
|
|
- `gateway/v1/auth.py` — `GET /api/v1/auth/Discord/status` (linked services lookup) and `POST /api/v1/auth/reset` (dev only)
|
|
- `gateway/jellystat/api.py` — `GET /jellystat/{history,genres,summary}/{user_id}` called internally by the `watch_history` skill
|
|
|
|
### JellyStat (`gateway/jellystat/`)
|
|
|
|
PostgreSQL connection pool stored on `app.state.jellystat_pool`. Database functions (`startup-functions.sql`) are deployed on startup via `CREATE OR REPLACE FUNCTION`. The watch_history skill calls these endpoints over HTTP (localhost) rather than querying the DB directly, keeping DB credentials isolated from the skill layer.
|
|
|
|
### Seerr session caching (`agents/skills/seerr.py`)
|
|
|
|
`httpx.AsyncClient` instances are cached per event loop (the Discord bot thread has its own loop separate from FastAPI). Cookie-based auth for Seerr is obtained once at startup via a sync login (thread-safe with double-check locking), then reused across all event-loop-specific clients.
|
|
|
|
## Key patterns
|
|
|
|
- **Self-registration**: agents, skills, and auth services all register at import time via module-level function calls. New modules just need to be imported once (see `load_all_agents()` and `import gateway.auth.jellyfin` in `main.py`).
|
|
- **Auth gate**: skills declare `requires_auth=["jellyfin"]` and the framework checks credentials before tool execution. Tools receive `_discord_user_id` injected into their args dict.
|
|
- **TMDb IDs as source of truth**: media tools display `[tmdb:123456]` tags and prefer IDs over title matching. The system prompt instructs the LLM to always show and use these IDs.
|