bot: queued AI query interface + librarian AI review of results
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:
@@ -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():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user