4.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
# 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 asyncexecutecallback. Each skill self-registers viaregister()at import time. - Agents and skills are loaded at startup by
load_all_agents()inmain.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 APIwatch_history— Jellyfin watch stats via the internal JellyStat APImedia_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) andPOST /api/v1/auth/reset(dev only)gateway/jellystat/api.py—GET /jellystat/{history,genres,summary}/{user_id}called internally by thewatch_historyskill
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()andimport gateway.auth.jellyfininmain.py). - Auth gate: skills declare
requires_auth=["jellyfin"]and the framework checks credentials before tool execution. Tools receive_discord_user_idinjected 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.