Wire the bot's AI chat pipeline (ai_functions.handle_response) to talk to
either OpenAI or the Anthropic Messages API, chosen by one active-config
switch. Behaviour on the default "gpt" config is unchanged.
constants.py:
* guarded `import anthropic` + CLAUDECLIENT (mirrors OPENAICLIENT), netrc
machine 'anthropic' / ANTHROPIC_API_KEY;
* CLAUDE_LATEST_MODEL / CLAUDE_CHEAP_MODEL (opus-4-8 / haiku-4-5);
* AI_CONFIGS + DEFAULT_AI_CONFIG loaded from an optional 3rd element of
system_gpt_settings.json (backward compatible - a 2-element file falls
back to built-in defaults, active "gpt"). Single switch: CONJURER_AI_CONFIG
env > settings "active" > "gpt".
ai_functions.py:
* provider_generate() dispatches to OpenAI (unchanged openai_call) or the new
_anthropic_call() (splits system out, alternating messages, max_tokens,
temperature omitted - Opus 4.8 rejects sampling params);
* AIError normalises both SDKs' exceptions into one category set so
handle_response keeps its single set of in-character error replies;
* select_model() reads the active config; legacy "gpt-4o" default auto-maps
to the active provider's model so the switch actually changes the backend;
* set_active_ai_config()/list_ai_configs() with best-effort persistence back
into system_gpt_settings.json index 2.
ai_commands.py:
* $gadaj_teraz <config> hybrid command (Vykidailo-gated) switches backend at
runtime;
* graceful guards when OPENAICLIENT is None: personal assistants (OpenAI
Assistants API) and DALL-E image gen degrade instead of crashing, so a
Claude-only deployment boots.
system_gpt_settings.json: add the configs block (gpt/claude/_template) as the
collection point for future backends. requirements_bot.txt: add anthropic.
bot.env.example: ANTHROPIC_API_KEY + CONJURER_AI_CONFIG. Unit tests cover the
message splitter, model selection, config listing, and error mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Permissions post-mortem that motivated this: the musician wrote radio
playlists AS ROOT onto a ROOT-OWNED network share which liquidsoap then
read AS USER 'radio' - chown fails on such shares by design (root squash /
uid mapping), so the radio came up and died on the playlists. The fix is
structural: the playlist WRITER now lives in the same container as the
READER, as the same user, on a local volume. No shared partition, no
chown, no uid mapping.
New: conjurer_betoniarka/betoniarka.py - runs inside the radio container
(started by the entrypoint as user 'radio', port 5005):
- library scan -> all_playlist/hit playlists with LOCAL container paths
(start + every 24h + authenticated GET /rescan)
- bot-facing radio API moved from the musician: /add_to_priority,
/create_priority_playlist, /request_radio_file, /clear_pr_pls,
plus GET /ping (health) and /stream (web page)
- radio_log/persistence tailer forwarding play events to the bot's
/prepped_tracks with the shared API key (bot-unreachable = logged, not fatal)
Musician: pure Discord music player now - keeps /mp3, /update_mp3,
/get_music and the file-share endpoints; all radio playlist writing, radio
paths/env and the tailer removed.
Bot: new CONJURER_RADIO_SERVICE (defaults to CONJURER_FILE_SERVICE so
un-split deployments keep working); radio_commands targets it; separate
'radio' health-gate group on betoniarka /ping (musician group now covers
music_commands + file_search_commands only).
Docker: betoniarka baked into the radio image (python3 + flask/waitress/
requests from Debian debs), port 5005 exposed, entrypoint starts it via
setpriv as 'radio'; data-volume chown is now best-effort with a loud
warning (keep the volume local); docs get the post-mortem + wiring.
Verified: py_compile everything; functional stub tests - rescan writes
local-path playlists, wyszukaj scores and appends to priority, auth
401/ok, tailer forward carries the API key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cog did its own netrc read from a hardcoded /home/pi/.netrc at import
time, crashing on any other host. Now:
- constants.py: ASSEMBLYAI_API_KEY resolved like every other token
(env ASSEMBLYAI_API_KEY -> netrc machine 'assemblyai' at
CONJURER_NETRC_FILE); new TRANSCRIPTS_PATH (env
CONJURER_TRANSCRIPTS_PATH, defaults next to the log file, created by the
runtime layout)
- voice_recognition_commands.py: drop the hardcoded netrc read and
transcript dir; when the key is missing raise a clear RuntimeError so
the guarded loader disables ONLY this cog with a readable reason
- bot.env.example: document ASSEMBLYAI_API_KEY
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Containerises the three services (each intended for its own Proxmox VM)
and adds the code changes needed to run cleanly on Linux/Docker.
Code fixes:
- constants.py: CONJURER_DATA_DIR roots all writable bot state under one
mounted volume (per-variable overrides still win; native Pi unaffected)
- conjurer_librarian/search_bot.py + scrape_bot.py: the hardcoded Windows
DOI database path (C:\Database\chunks\) is now CONJURER_LIBRARIAN_DB_PATH,
with CONJURER_LIBRARIAN_MAXTHREADS / _CHUNK also env-overridable
Docker:
- docker/Dockerfile.{bot,librarian,musician} + compose.{bot,librarian,musician}.yaml
- docker/env/*.env.example (force-added; real *.env stays gitignored)
- docker/entrypoint.bot.sh seeds default JSON state into /data only when
absent, so preserved history is never overwritten
- .dockerignore
- docs/deployment/DOCKER_PROXMOX.md: step-by-step runbook incl. preserving
the existing command/conversation history and cross-VM auth
The bot image uses the vendored yt_dlp/spotify_dl forks (they win on
sys.path over the pip packages), dropping the old sed patching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>