bot: queued AI query interface + librarian AI review of results
CI / integration (push) Failing after 2m23s
CI / compile (push) Failing after 1h53m43s
CI / unit (push) Failing after 1h53m32s
build / build (push) Failing after 1h54m2s

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>
This commit was merged in pull request #2.
This commit is contained in:
2026-07-30 00:26:20 +02:00
committed by gitea
parent 1a59c9f6c5
commit b13a8afa01
5 changed files with 304 additions and 4 deletions
+57 -1
View File
@@ -17,6 +17,11 @@ ICECAST_ADDRESS = os.getenv("CONJURER_ICECAST", "http://192.168.1.12:8000")
API_KEY = os.getenv("CONJURER_API_KEY")
OUT_COMM_Q = Queue()
IN_COMM_Q = Queue()
# AI request queue: prompts to be answered by the bot's own AI backend (whatever
# $gadaj_teraz currently points at). Drained by the AI cog's worker loop, which
# calls handle_response and delivers the answer to the requested channel. Fed
# either over HTTP (POST /ai_query) or in-process via submit_ai_query().
AI_QUERY_Q = Queue()
SRCHTITLE = re.compile(rb"StreamTitle=\\*(?P<title>[^;]*);").search
awaiting_q = []
@@ -47,13 +52,17 @@ class QueryControl:
content, logger, context, and replies.
"""
def __init__(self, query_author, query_uuid, query_content, ctx) -> None:
def __init__(self, query_author, query_uuid, query_content, ctx, ai_review=False) -> None:
self.author = query_author
self.uuid = query_uuid
self.content = query_content
self.logger = logging.getLogger("discord")
self.stop = False
self.ctx = ctx
# When True, once the librarian returns hits, the DOI list + the search
# phrase are sent to the AI backend for a weighted-relevance re-rank and
# source review (see librarian_commands.check_data_q).
self.ai_review = ai_review
self.logger.info(
f"Created Query control for {self.author}, {self.uuid}: {self.content}"
)
@@ -105,6 +114,53 @@ def answer_external_command():
return jsonify("SUCCESS")
def submit_ai_query(prompt, channel_id=None, request_type="NONE", username="conjurer", query_uuid=None):
"""Queue an AI prompt for the bot to answer with its configured backend.
In-process entry point (used by the librarian result handler). ``channel_id``
is the Discord channel the answer should be posted to; ``request_type`` is
passed through to handle_response ("NONE" keeps it a clean one-shot that does
not touch conversation memory).
"""
AI_QUERY_Q.put(
{
"uuid": query_uuid,
"prompt": prompt,
"channel_id": channel_id,
"request_type": request_type,
"username": username,
}
)
@app.route("/ai_query", methods=["POST"])
def ai_query():
"""Inbound AI query: queue a prompt to be answered by the bot's AI backend.
Payload: {"prompt": str, "channel_id": int, "request_type"?: str,
"username"?: str, "uuid"?: str}. The prompt is queued and answered
asynchronously by the AI cog's worker; the answer is posted to channel_id.
"""
_authorize_request()
logger = logging.getLogger("discord")
record = json.loads(request.data)
prompt = record.get("prompt", "")
if not prompt:
return jsonify(isError=True, message="missing 'prompt'", statusCode=400, data=[]), 400
submit_ai_query(
prompt=prompt,
channel_id=record.get("channel_id"),
request_type=record.get("request_type", "NONE"),
username=record.get("username", "external"),
query_uuid=record.get("uuid"),
)
logger.info("Queued AI query (qsize=%s)", AI_QUERY_Q.qsize())
return (
jsonify(isError=False, message="Queued", statusCode=200, data={"qsize": AI_QUERY_Q.qsize()}),
200,
)
@app.route("/conjurer", methods=["GET"])
def check_alive():
"""