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
+65 -1
View File
@@ -9,8 +9,10 @@ from pathlib import Path
import discord
import openai
import requests
from discord.ext import commands
from queue import Empty
from discord.ext import commands, tasks
from other_functions import discord_friendly_send, discord_friendly_reply
from communication_subroutine import AI_QUERY_Q
import ai_functions
@@ -43,7 +45,66 @@ class Events(commands.Cog):
self.armia[superfryta[0]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK
self.logger.info(self.armia)
@tasks.loop(seconds=2)
async def ai_query_worker(self):
"""Drain AI_QUERY_Q one prompt at a time and answer with the active backend.
This is the bot-side half of the AI query interface: prompts arrive over
HTTP (POST /ai_query) or in-process (submit_ai_query), get queued, and are
answered here with handle_response - so they automatically use whichever
provider $gadaj_teraz currently selects. The answer is posted to the
channel the request named.
"""
try:
item = AI_QUERY_Q.get(block=False)
except Empty:
return
prompt = item.get("prompt", "")
request_type = item.get("request_type", "NONE")
channel_id = item.get("channel_id")
username = item.get("username", "conjurer")
self.logger.info(
"AI query from %s (%s) -> channel %s", username, request_type, channel_id
)
global MESSAGE_TABLE # pylint: disable=global-statement
try:
if request_type == "NONE":
# Clean one-shot: no persona system prompt, no memory write.
result, _ = await ai_functions.handle_response(
"", True, True, [], username, "NONE", none_request=prompt
)
else:
result, MESSAGE_TABLE = await ai_functions.handle_response(
prompt, True, True, MESSAGE_TABLE, username, request_type
)
except Exception as exc: # pylint: disable=broad-except
self.logger.exception("AI query failed: %s", exc)
result = "*Kondziu drapie się po głowie* Coś się zjebało przy pytaniu do AI."
if channel_id is None:
self.logger.warning("AI query had no channel_id - answer dropped")
return
channel = self.bot.get_channel(channel_id)
if channel is None:
self.logger.warning("AI query channel %s not found - answer dropped", channel_id)
return
await self._send_chunked(channel, result)
@ai_query_worker.before_loop
async def _before_ai_query_worker(self):
await self.bot.wait_until_ready()
async def _send_chunked(self, channel, text):
"""Send text in <=1900-char pieces (Discord caps messages at 2000)."""
text = text or ""
while text:
await discord_friendly_send(channel, text[:1900])
text = text[1900:]
async def cog_load(self):
# The AI query worker must run regardless of the OpenAI guard below - it
# answers via handle_response, which works on Claude too. Start it first.
if not self.ai_query_worker.is_running():
self.ai_query_worker.start()
self.logger.info("Starting personal assistants")
# Personal assistants use the OpenAI Assistants API (threads/runs), which
# has no Anthropic equivalent - skip cleanly when OpenAI isn't wired up
@@ -87,6 +148,9 @@ class Events(commands.Cog):
)
self.logger.info("Started personal assistants")
async def cog_unload(self):
self.ai_query_worker.cancel()
@commands.hybrid_command(
name="switch_dm_mode",
description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj",