Files
TimHoogervorst f21676eafd
Build and Push Agent API / build (push) Successful in 4s
removed bunch of old code, added /init bunch of cleanups
2026-05-25 18:07:01 +02:00

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 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.pyGET /api/v1/auth/Discord/status (linked services lookup) and POST /api/v1/auth/reset (dev only)
  • gateway/jellystat/api.pyGET /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.