Two things the field report asked for.
1) PERSONAL ASSISTANTS (replacing the sunset OpenAI Assistants API)
The old implementation gave three capabilities. Two are reimplemented here,
the third was confirmed unused and is deliberately not replaced:
* per-user persona - it already lived in system_gpt_settings.json; it was
only ever being shipped to OpenAI. It is now the system prompt.
* per-user conversation thread - OpenAI held this server-side. It now lives
in assistant_memory.json, keyed by discord user id, trimmed to the most
recent turns (CONJURER_ASSISTANT_MEMORY_TURNS) and written atomically so a
torn write cannot lose someone's history. Deliberately a plain trim, not
the AI summarisation used for the bar's shared memory: these are private
DMs and must not end up in a public "legend".
* file_search - not replaced. Confirmed not in use.
The conversation goes through handle_response with request_type="NONE" and an
explicit message list, which keeps it out of the bar's shared memory. The big
win: create_chat_assistant hardcoded model="gpt-4o", so assistants were locked
to OpenAI. They now run on whatever $gadaj_teraz selects - Claude and Ollama
included.
create_chat_assistant / chat_with_assistant are gone, and with them the last
call to beta.threads in the startup path - so the cog cannot be killed by that
API again. (add_files_to_vector_store / delete_files_from_vector_store still
reference beta.assistants but are dead code - nothing calls them - so they
cannot crash anything; left alone rather than widening this change.)
2) KEEPING A SELF-HOSTED MODEL WARM
Loading is the slow part - the GPU is shared with other users - so we preload
via Ollama's documented mechanism: /api/generate with a model, a keep_alive
and NO prompt. It loads the model and generates nothing.
* on switching to ollama, $gadaj_teraz fires a preload in the BACKGROUND
(not awaited: loading can take minutes and the command must answer at
once), so the wait lands on the operator rather than the first user;
* a warm loop re-asserts keep_alive every CONJURER_OLLAMA_WARM_MINUTES.
Both are hard-guarded on the ACTIVE provider being ollama. Warming a metered
API would burn tokens and money for nothing, so that guard is pinned by a test
asserting the preload is never called for gpt/claude, and another asserting the
preload body carries no prompt (a prompt would make every warm-up generate).
Tests: 82 unit + 71 integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field report from both instances: "Command modele_ai is not found", and on
restart the extension fails outright:
ai_commands.py:129 in cog_load
thread = await OPENAICLIENT.beta.threads.create()
openai.NotFoundError: Error code: 404
-> ExtensionFailed: Extension 'ai_commands' raised an error
The personal-assistants bootstrap calls the OpenAI Assistants API (beta
threads/runs), a legacy surface that now answers 404. That exception
propagated out of cog_load, so discord.py failed the whole extension - and
with it EVERY AI command: $gadaj_teraz, $modele_ai and the conversation
handler. The bot kept running (bot.py loads each extension defensively), it
simply had no AI at all.
cog_load already had the right instinct - it skips the bootstrap cleanly
when OPENAICLIENT is None, so a Claude-only deployment works - but it
guarded against the client being ABSENT, not against the call FAILING.
Move the bootstrap into _start_personal_assistants() and treat any failure
there as non-fatal: log what was lost and carry on. Personal assistants are
one optional feature; the rest of the cog works fine on Claude and Ollama
and must not go down with them.
Not unit-tested on purpose: exercising cog_load needs stubs for openai,
tiktoken and a tasks.loop complete enough to answer is_running(), at which
point the test exercises the stubs rather than the code. Verified against
the running cluster instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switching to a backend whose configured model the server does not have
succeeded silently, and then every reply failed with "model not found" with
nothing explaining why. The switch already fetches the model list to show
what else is available, so use it: if the config's model is absent, say so
and list what IS there.
Found while probing the real server (192.168.1.72): it has exactly one
model, gemma4:e2b, so the built-in llama3.1:8b default would have hit this
on the first switch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bot could talk to OpenAI or Anthropic; this adds Ollama as a third
provider so it can run against models hosted on our own box, and extends
the switch command to pick WHICH model - not just which backend.
Provider: Ollama exposes an OpenAI-compatible /v1 surface, so the client is
just openai.AsyncOpenAI(base_url=OLLAMA_URL + "/v1"). That reuses the
existing message format and the whole _map_openai_error mapping instead of
forking a second error taxonomy. There is no API key - the endpoint IS the
configuration, so the backend stays dormant (and refuses to be selected,
with a message naming the variable) until CONJURER_OLLAMA_URL is set, the
same way the Conan bridge behaves.
Model selection:
* list_provider_models() asks the SERVER for Ollama (/v1/models), so the
picker shows what is actually pulled on the box rather than a hardcoded
list. Hosted providers just report what they are wired to.
* set_active_model() pins the config's latest_model and persists it;
cheap_model is left alone so the MUSIC path keeps its cheaper backend.
* $gadaj_teraz now takes "<config> [model]", and a new read-only $modele_ai
lists what is available. Pinning an id Ollama does not have is rejected up
front with the real list - otherwise the typo only surfaces later as a
failed reply.
Two fixes this exposed:
* AI_CONFIGS now merges built-in defaults with the settings-file block
instead of letting the file win outright. Every provider switch persists a
"configs" block, so a file written by an older build would have
permanently hidden ollama from the picker after an upgrade.
* _persist_active_ai_config assigns "configs" instead of setdefault, so a
pinned model actually survives a restart.
* the hardcoded 120s response timeout is now CONJURER_AI_TIMEOUT_SECONDS - a
self-hosted model on a modest GPU can legitimately need longer.
Tests cover: ollama appears in the picker, select_model maps the legacy
gpt-4o default instead of leaking it, model listing (server-queried, sorted,
de-duplicated, failure -> AIError, unconfigured -> auth), pinning (latest
only, blank/unknown rejected), and that provider_generate routes to the new
path. Suite: 68 unit + 70 integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seven confirmed defects from the code audit, each small and low-risk.
* ai_functions.get_random_cyclic_message: random.randint(0, len(CYCLIC_WORDS))
is inclusive -> could return len -> IndexError. Now randrange(len) + guard on
an empty CYCLIC_WORDS.
* librarian_commands.get_image_sadox: random.randrange(0, len(res)-1) never
picked the last comic and raised ValueError('empty range') on a single file.
Now randrange(len) + an empty-dir guard.
* ai_commands image generation: every DALL-E error branch replied but did not
return, so control fell through to `if response:` with response unbound ->
UnboundLocalError right after the friendly message. Each branch now returns;
response is pre-initialised; and PermissionDeniedError no longer passes a
(message, text) tuple as a single arg.
* search_bot DOI match: `item["DOI"] in data` was a substring test, so a DOI
that is a prefix of a longer one (10.1/1 vs 10.1/12) produced a false 'exists'
hit. Now matches the line's first whitespace token exactly, via an O(1) dict
index built once per consumer (also removes the O(queried-DOIs) per-line scan
- a real win for large databases).
* communication_subroutine.scan_incoming: matched records were never removed
from awaiting_q, so it grew unbounded over uptime and a reused UUID could
re-match a stale record. Matched records are now dropped after dispatch.
* communication_subroutine.id3: (resp.headers.get("icy-name") or "").title()
guards against a stream that omits headers (was AttributeError on None,
500-ing the /prepped_tracks "next" handler).
* betoniarka.scan_tracks: waits for the radio logs to exist instead of dying
with FileNotFoundError on a fresh deploy (which silently killed the
now-playing forwarder until a restart).
Verified: tests/unit/test_search_bot.py gains exact-match and trailing-metadata
cases; full unit job 43 passed. Remaining observations (image-gen stale
/home/pi fallback paths + dead FileNotFoundError-after-OSError branch; tailer
still vulnerable to mid-run log rotation; DOI-first-token assumption) noted for
follow-up - none are crashes on the normal path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two connected features.
1) AI query interface (via the comm layer). communication_subroutine gains an
AI_QUERY_Q, a submit_ai_query() in-process entry point, and an authed
POST /ai_query endpoint ({prompt, channel_id, request_type?, username?}). The
prompt is queued and answered asynchronously by a new tasks.loop worker in the
always-loaded AI cog (Events), which calls handle_response - so it runs on
whichever backend $gadaj_teraz currently selects (GPT or Claude) - and posts the
answer to the requested channel, chunked to Discord's limit. request_type "NONE"
(default) is a clean one-shot: no persona system prompt, no memory write. The
worker starts before the OpenAI guard in cog_load, so it also runs on a
Claude-only box; cog_unload cancels it.
2) Librarian AI review. New command $wyszukaj_z_recenzja mirrors
$wyszukaj_linki_do_dokumentow but sets ai_review=True on the QueryControl, which
rides the round-trip and is matched back by UUID. When the hits return,
check_data_q sends the raw list as before, then - if flagged - hands the same
list (already in Crossref-relevance order) plus the search phrase to the AI
queue for a weighted re-rank and per-source review, delivered to the same
channel. QueryControl gains an ai_review flag (default False, so the orphan path
and all existing callers are unaffected).
Confirmed separately (and noted in the docs): the DOI list the AI receives is
pre-sorted by Crossref relevance - the librarian pipeline only filters (drops
title-less items) and splits (in-db / not-in-db), never re-sorts, and relies on
insertion-ordered dicts (Py 3.7+).
Verified: /ai_query auth (401/200/400/open), submit_ai_query and the queued
dict shape, and the QueryControl flag - via a Flask test client and
tests/integration/test_ai_query_endpoint.py (5 tests, all pass; integration
suite 11 passed, the 3 failures are the pre-existing /clear_pr_pls musician
tests fixed on a separate branch). Full first-party compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quality-of-life: checking which AI backend is live no longer requires
switching to it. Bare `$gadaj_teraz` replies with the active config and the
selectable ones; that read-only path is open to everyone, while switching
stays Vykidailo-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Make the stable 'working copy' bot the canonical code at the repository
root so the install/deploy scripts operate against it again.
- Move working_copy/* to root (bot entrypoint is bot.py)
- Restore root-level install/ops scripts from c4fa88e (deploy.sh,
install_main_bot.sh, status_report.*, conjurer.service, etc.)
- Fix deploy.sh: copy bot.py (was thin_client.py) and add the
conanjurer_* modules; bump command count
- Remove side-by-side variant dirs (backup_old_docker, prototype_one,
prototype_musician_one, musician_old, working_copy) and docker cruft
- Keep components as subdirs: conjurer_librarian, conjurer_musician,
spotify_dl, yt_dlp, fonts, utils, docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>