# 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 ` (Quick Connect) and `/logout `. ### 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.