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:
+65
-1
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user