Files
ai-agents/agents/pirate/tools/radarr.py
T
Eric Jungbauer eac7b64a90 The Pirate — Phase 1.a: conversational read-only media agent
Ships a chat-based agent at /pirate that LLM-routes user questions to media-stack tools and returns natural-language answers grounded in real data. Foundation built on top of the existing API-tokens + dual-auth infrastructure so other apps (Open WebUI, HA voice, Synap) can consume the same Pirate API.

New subsystem (not the standard trigger/result pattern):
- pirate_conversations + pirate_messages tables
- service_configs table (admin-wide creds shared by media agents)
- /api/pirate/chat + /api/pirate/conversations/* (dual-auth: user session OR Bearer token scoped to user's pirate instance)
- /api/internal/pirate/* endpoints used by runtime subprocess
- /api/admin/services + Services tab in admin.html for cred management
- Auto-seeded service_configs on startup from Media Stack Reference defaults (never overwrite admin edits)
- Auto-seeded pirate catalog entry + per-user pirate instance on startup

Pirate package (agents/pirate/):
- prompts.py: system prompt, enforces read-only in Phase 1
- runtime.py: Anthropic-native tool-use loop (max 8 iterations, persists every turn)
- tools/_common.py: service_configs fetch + qBit session auth
- tools/sonarr.py: queue, upcoming, series_search, library_stats
- tools/radarr.py: queue, movie_search, library_stats
- tools/qbittorrent.py: torrents, transfer_stats, categories
- tools/storage.py: disk_space (via Sonarr diskspace API)
- Default model: claude-sonnet-4-5 (Haiku fumbles multi-step chains)

Dashboard:
- static/pirate.html — full chat UI with conversation sidebar, suggestion chips, inline tool-call visualization, 24h idle reset + New Chat button
- Pirate button added to main dashboard header

Wiki reorg: Agents / Developer Guides / Plans parent docs, per-agent reference docs, The Pirate doc. API Clients + Calling Agents docs moved under Developer Guides.

Working folder: PIRATE_PHASE_1A.md + NEXT_SESSION_PROMPT.md for fast bootstrap.

Smoke tested end-to-end: real tool calls against qBittorrent (13 active torrents correctly reported) and Sonarr disk-space; multi-turn conversation state preserved across follow-up questions.

On deck: Phase 1.b (Lidarr/Whisparr/Overseerr/Plex tools), then 1.d (OWUI pipeline), then 1.c (HA voice).
2026-04-20 19:01:50 +00:00

88 lines
3.2 KiB
Python

"""Radarr tools — movie queries (read-only)."""
from ._common import arr_get
def _compact_movie(m):
return {
"id": m.get("id"),
"title": m.get("title"),
"year": m.get("year"),
"monitored": m.get("monitored"),
"has_file": m.get("hasFile"),
"status": m.get("status"),
"size_on_disk_gb": round((m.get("sizeOnDisk", 0) or 0) / 1e9, 1),
}
def radarr_queue(limit=20):
"""Movies currently downloading or pending import."""
data = arr_get("radarr", "/api/v3/queue", {"pageSize": limit, "includeMovie": "true"})
items = []
for r in data.get("records", []):
items.append({
"title": r.get("title"),
"movie": r.get("movie", {}).get("title") if r.get("movie") else None,
"status": r.get("status"),
"timeleft": r.get("timeleft"),
"size_gb": round((r.get("size", 0) or 0) / 1e9, 2),
"size_left_gb": round((r.get("sizeleft", 0) or 0) / 1e9, 2),
"download_client": r.get("downloadClient"),
})
return {"total_records": data.get("totalRecords", 0), "items": items}
def radarr_movie_search(query):
"""Search the user's Radarr library for a movie by title. Does NOT search indexers."""
all_movies = arr_get("radarr", "/api/v3/movie")
q = query.lower().strip()
hits = [m for m in all_movies if q in (m.get("title") or "").lower()]
return {"query": query, "count": len(hits), "movies": [_compact_movie(m) for m in hits[:20]]}
def radarr_library_stats():
"""Summary stats of the Radarr movie library."""
all_movies = arr_get("radarr", "/api/v3/movie")
on_disk = sum(1 for m in all_movies if m.get("hasFile"))
total_size = sum((m.get("sizeOnDisk", 0) or 0) for m in all_movies)
missing = [m.get("title") for m in all_movies if not m.get("hasFile") and m.get("monitored")][:10]
return {
"movie_count": len(all_movies),
"movies_on_disk": on_disk,
"movies_missing_monitored": len([m for m in all_movies if not m.get("hasFile") and m.get("monitored")]),
"total_size_tb": round(total_size / 1e12, 2),
"sample_missing": missing,
}
TOOLS = [
{
"name": "radarr_queue",
"description": "List movies Radarr is currently downloading or importing. Use when the user asks about movie downloads.",
"input_schema": {
"type": "object",
"properties": {"limit": {"type": "integer", "default": 20}},
},
"read_only": True,
"fn": radarr_queue,
},
{
"name": "radarr_movie_search",
"description": "Check whether a specific movie is in the user's Radarr library. Does NOT search for new movies to add.",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
"read_only": True,
"fn": radarr_movie_search,
},
{
"name": "radarr_library_stats",
"description": "High-level stats about the Radarr movie library: how many movies, how many on disk, total size.",
"input_schema": {"type": "object", "properties": {}},
"read_only": True,
"fn": radarr_library_stats,
},
]