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:
+94
-2
@@ -14,7 +14,7 @@ import requests
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
from ai_functions import handle_response
|
||||
from communication_subroutine import IN_COMM_Q, OUT_COMM_Q, QueryControl
|
||||
from communication_subroutine import IN_COMM_Q, OUT_COMM_Q, QueryControl, submit_ai_query
|
||||
from constants import DIR_PATH_SADOX, LIBRARIAN_SERVICE_ADDRESS, SEND_QUERY, service_headers
|
||||
|
||||
SERVICE_HEADERS = service_headers()
|
||||
@@ -97,14 +97,18 @@ class DataModule(commands.Cog):
|
||||
if fresh_data.stop:
|
||||
searcher = fresh_data.author
|
||||
query = fresh_data.content
|
||||
# ai_lines is a clean, plain rendering of the SAME list in the
|
||||
# SAME (Crossref-relevance) order, for the optional AI review.
|
||||
ai_lines = []
|
||||
l_p = 1
|
||||
for doi in fresh_data.entries:
|
||||
self.logger.info(doi)
|
||||
desc = fresh_data.entries[doi]
|
||||
title = desc["Title"][0]
|
||||
title = desc["Title"][0] if desc.get("Title") else "(bez tytułu)"
|
||||
entries.append(
|
||||
f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n"
|
||||
)
|
||||
ai_lines.append(f"{l_p}. {title} (DOI: {doi}, typ: {desc['type']})")
|
||||
l_p += 1
|
||||
message = "*Z podłogi wysuwa się winda na książki*"
|
||||
if fresh_data.ctx is not None:
|
||||
@@ -131,6 +135,36 @@ class DataModule(commands.Cog):
|
||||
await ctx.send(message)
|
||||
message = ""
|
||||
|
||||
# Optional AI pass: re-rank the (already Crossref-relevance-
|
||||
# sorted) DOI list and review the sources. Enqueued to the AI
|
||||
# worker so it runs on whatever backend $gadaj_teraz selected;
|
||||
# the answer lands in this same channel.
|
||||
if getattr(fresh_data, "ai_review", False) and ai_lines:
|
||||
target = getattr(ctx, "channel", ctx)
|
||||
review_prompt = (
|
||||
f'Poniżej lista źródeł naukowych znalezionych dla zapytania: "{query}".\n'
|
||||
"Lista jest już wstępnie posortowana według trafności wg Crossref "
|
||||
"(od najtrafniejszej).\n\n"
|
||||
"Twoje zadania:\n"
|
||||
"1. Przeważ i uporządkuj listę według RZECZYWISTEJ trafności do zapytania "
|
||||
"(najtrafniejsze u góry).\n"
|
||||
"2. Do każdej pozycji dopisz jedno-, dwuzdaniową recenzję: typ i wiarygodność "
|
||||
"źródła oraz dlaczego (nie) pasuje do zapytania.\n"
|
||||
"Odpowiedz zwięźle, numerowaną listą, po polsku.\n\n"
|
||||
"Źródła:\n" + "\n".join(ai_lines)
|
||||
)
|
||||
submit_ai_query(
|
||||
prompt=review_prompt,
|
||||
channel_id=target.id,
|
||||
request_type="NONE",
|
||||
username=searcher,
|
||||
query_uuid=str(fresh_data.uuid),
|
||||
)
|
||||
await ctx.send(
|
||||
"*Conjurer podaje listę naszemu rezydentowi-mądrali od AI* "
|
||||
"Za chwilę dorzuci recenzję i swoje przesortowanie wg trafności."
|
||||
)
|
||||
|
||||
# Kept for sentimental reasons
|
||||
# await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}")
|
||||
except Empty:
|
||||
@@ -199,6 +233,64 @@ class DataModule(commands.Cog):
|
||||
+ " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni."
|
||||
)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="wyszukaj_z_recenzja",
|
||||
description="Jak wyszukaj_linki_do_dokumentow, ale wyniki przesortuje trafnością i zrecenzuje AI",
|
||||
guild=discord.Object(id=664789470779932693),
|
||||
)
|
||||
async def wyszukaj_z_recenzja(self, ctx):
|
||||
"""Same as wyszukaj_linki_do_dokumentow, but flags the search for an AI
|
||||
review: when the DOI hits come back, the list + the search phrase are sent
|
||||
to the bot's AI backend for a weighted-relevance re-rank and a source
|
||||
review, delivered to this channel. The flag rides on the QueryControl so
|
||||
it survives the round-trip and is matched back to this search by UUID.
|
||||
"""
|
||||
query = ctx.message.content
|
||||
query_uuid = uuid.uuid4()
|
||||
ctx.message.content = ctx.message.content.replace("$wyszukaj_z_recenzja", "")
|
||||
|
||||
json_query = {
|
||||
"UUID": str(query_uuid),
|
||||
"query": str(query),
|
||||
"page": 1,
|
||||
"deep_search": False,
|
||||
}
|
||||
coroutine = asyncio.to_thread(
|
||||
requests.post,
|
||||
f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}",
|
||||
json=json_query,
|
||||
headers=SERVICE_HEADERS,
|
||||
timeout=360,
|
||||
)
|
||||
await ctx.send(
|
||||
"*Conjurer notuje, wrzuca liścik do rury pneumatycznej i mruży oko* Tym razem jak coś"
|
||||
+ " znajdę, przepuszczę wyniki jeszcze przez naszego rezydenta-mądralę od AI - przeważy"
|
||||
+ " trafność i zrecenzuje źródła. Poczekaj kilka godzin - biblioteka to 3/4 stacji."
|
||||
)
|
||||
query_response = await coroutine
|
||||
if not query_response.status_code == 200:
|
||||
await ctx.send(
|
||||
"*Z rury wydobywa się dym. Conjurer pryska w nią pierwszą cieczą pod ręką i wybucha"
|
||||
+ " drobny pożar.* Wołaj szefa - mam wrażenie że się coś wyjebało"
|
||||
)
|
||||
return
|
||||
|
||||
query, query_uuid, queue_size = (
|
||||
query_response.json()["data"][0],
|
||||
query_response.json()["data"][1],
|
||||
query_response.json()["data"][2],
|
||||
)
|
||||
if ctx.message.author.nick:
|
||||
username = ctx.message.author.nick
|
||||
else:
|
||||
username = ctx.message.author.name
|
||||
query_object = QueryControl(username, query_uuid, query, ctx, ai_review=True)
|
||||
OUT_COMM_Q.put(query_object)
|
||||
await ctx.send(
|
||||
f"Poszło z recenzją AI. Identyfikator: {query_uuid}. Jesteś {queue_size} w kolejce."
|
||||
+ " Najpierw dojadą surowe wyniki, a zaraz po nich przesortowanie i recenzja od AI."
|
||||
)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="glebokie_gardlo",
|
||||
description="Przygotowuje drinka o nazwie głębokie gardło",
|
||||
|
||||
Reference in New Issue
Block a user