Igra: trenutna potvrda izbora (prepoznavanje odgođeno) + timeout web searcha
Uzrok sporosti (iz loga): prepoznavanje lika, uključujući web search, radilo se UNUTAR superheroji_izbor prije potvrde. Za nepoznato ime web search je jednom trajao ~100s i blokirao potvrdu. - record_pick je sada BRZ (bez LLM-a): odmah bilježi izbor i vraća potvrdu; brza provjera zabrane/duplikata preko keša alias-a i izgovorenog imena. - Pravo prepoznavanje (LLM + web search) radi se pri presudi (_resolve_pending), paralelno, gdje ionako ima filler poruka. - Web search ograničen na 20s (+8s wiki fallback), da presuda ne visi. - Filler je sada periodičan (svakih ~9s dok traje presuda), ne samo jednom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
@@ -233,16 +233,24 @@ class Assistant:
|
|||||||
nxt = self.match.next_picker()
|
nxt = self.match.next_picker()
|
||||||
return confirm + f" Zatim pitaj igrača {nxt} da kaže svog lika."
|
return confirm + f" Zatim pitaj igrača {nxt} da kaže svog lika."
|
||||||
|
|
||||||
|
_FILLERS = [
|
||||||
|
"Reci kratko i veselo, u jednoj rečenici, da još razmišljaš ko bi pobijedio. Ne otkrivaj pobjednika.",
|
||||||
|
"Reci u jednoj rečenici da je borba tijesna i da još vagaš ko je jači. Ne otkrivaj pobjednika.",
|
||||||
|
"Reci kratko da samo što nisi odlučio, još malo. Ne otkrivaj pobjednika.",
|
||||||
|
]
|
||||||
|
|
||||||
async def _adjudicate_and_announce(self, match: "game.Match") -> None:
|
async def _adjudicate_and_announce(self, match: "game.Match") -> None:
|
||||||
"""Presudi rundu (može potrajati). Ako traje, ubaci filler; onda pošalji rezultat modelu."""
|
"""Presudi rundu (može potrajati zbog prepoznavanja/web searcha). Ubacuj filler dok traje."""
|
||||||
try:
|
|
||||||
adj = asyncio.create_task(match.resolve_round())
|
adj = asyncio.create_task(match.resolve_round())
|
||||||
|
i = 0
|
||||||
try:
|
try:
|
||||||
res = await asyncio.wait_for(asyncio.shield(adj), timeout=8)
|
while True:
|
||||||
|
try:
|
||||||
|
res = await asyncio.wait_for(asyncio.shield(adj), timeout=9)
|
||||||
|
break
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await self._nudge("Reci kratko, veselo i u jednoj rečenici da još razmišljaš ko bi pobijedio "
|
await self._nudge(self._FILLERS[min(i, len(self._FILLERS) - 1)])
|
||||||
"(npr. 'Hmm, još vagam ko je jači...'). Ne otkrivaj pobjednika.")
|
i += 1
|
||||||
res = await adj
|
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
log.exception("presuda runde nije uspjela")
|
log.exception("presuda runde nije uspjela")
|
||||||
await self.notify_model("Došlo je do greške pri presudi runde. Reci djeci da još jednom kažu svoje likove.")
|
await self.notify_model("Došlo je do greške pri presudi runde. Reci djeci da još jednom kažu svoje likove.")
|
||||||
|
|||||||
@@ -32,28 +32,33 @@ znatno slabijeg lika bez izuzetno jakog razloga. Poštuj ranije ishode da igra b
|
|||||||
Vrati JSON: {"pobjednik": "tačno ime pobjednika iz date liste", "razlog": "obrazloženje na bosanskom"}."""
|
Vrati JSON: {"pobjednik": "tačno ime pobjednika iz date liste", "razlog": "obrazloženje na bosanskom"}."""
|
||||||
|
|
||||||
|
|
||||||
|
WEB_SEARCH_TIMEOUT = 20.0 # sekundi — da presuda ne visi predugo
|
||||||
|
|
||||||
|
|
||||||
async def _web_search(client: AsyncOpenAI, model: str, name: str) -> str:
|
async def _web_search(client: AsyncOpenAI, model: str, name: str) -> str:
|
||||||
"""Kratak opis lika sa weba — koristi se SAMO kad LLM ne zna lika."""
|
"""Kratak opis lika sa weba — koristi se SAMO kad LLM ne zna lika. Vremenski ograničeno."""
|
||||||
try:
|
try:
|
||||||
resp = await client.responses.create(
|
resp = await asyncio.wait_for(client.responses.create(
|
||||||
model=model,
|
model=model,
|
||||||
tools=[{"type": "web_search"}],
|
tools=[{"type": "web_search"}],
|
||||||
input=(f"Ko je izmišljeni lik '{name}'? Iz kojeg je djela (film, crtani, strip, igra, knjiga)? "
|
input=(f"Ko je izmišljeni lik '{name}'? Iz kojeg je djela (film, crtani, strip, igra, knjiga)? "
|
||||||
f"Ukratko opiši lika i njegove moći ili sposobnosti."),
|
f"Ukratko opiši lika i njegove moći ili sposobnosti."),
|
||||||
)
|
), timeout=WEB_SEARCH_TIMEOUT)
|
||||||
text = (resp.output_text or "").strip()
|
text = (resp.output_text or "").strip()
|
||||||
if text:
|
if text:
|
||||||
log.info("web search za %r: %d znakova", name, len(text))
|
log.info("web search za %r: %d znakova", name, len(text))
|
||||||
return text
|
return text
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
log.warning("web search za %r prekinut nakon %.0fs", name, WEB_SEARCH_TIMEOUT)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
log.exception("web search (responses API) nije uspio za %r", name)
|
log.exception("web search (responses API) nije uspio za %r", name)
|
||||||
# rezervni izvor: engleska Wikipedia
|
# rezervni izvor: engleska Wikipedia (brzo)
|
||||||
try:
|
try:
|
||||||
art = await asyncio.to_thread(wiki.find_article, name)
|
art = await asyncio.wait_for(asyncio.to_thread(wiki.find_article, name), timeout=8.0)
|
||||||
if art:
|
if art:
|
||||||
return f"{art.title}: {art.text[:800]}"
|
return f"{art.title}: {art.text[:800]}"
|
||||||
except Exception: # noqa: BLE001
|
except (asyncio.TimeoutError, Exception): # noqa: BLE001
|
||||||
log.exception("wiki fallback nije uspio za %r", name)
|
log.warning("wiki fallback nije uspio/prekinut za %r", name)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -237,28 +242,70 @@ class Match:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def record_pick(self, player: str, hero: str) -> dict:
|
async def record_pick(self, player: str, hero: str) -> dict:
|
||||||
|
"""BRZO bilježi izbor (bez poziva LLM-u), da potvrda bude trenutna.
|
||||||
|
|
||||||
|
Pravo prepoznavanje lika (LLM + web search) radi se kasnije, pri presudi runde
|
||||||
|
(_resolve_pending), gdje ionako ima vremena i filler poruka.
|
||||||
|
"""
|
||||||
player = (player or "").strip()
|
player = (player or "").strip()
|
||||||
# upari igrača (tolerantno na sitne razlike)
|
|
||||||
if player not in self.scores:
|
if player not in self.scores:
|
||||||
match = next((p for p in self.players if p.lower() == player.lower()), None)
|
match = next((p for p in self.players if p.lower() == player.lower()), None)
|
||||||
player = match or (self.players[0] if len(self.players) == 1 else player)
|
player = match or (self.players[0] if len(self.players) == 1 else player)
|
||||||
info = await resolve_hero(self.client, self.model, self.store, hero)
|
spoken = (hero or "").strip()
|
||||||
if info is None:
|
if not spoken:
|
||||||
return {"ok": False, "razlog": "nepoznat",
|
return {"ok": False, "razlog": "prazno",
|
||||||
"poruka": f"Ne mogu prepoznati lika '{hero}'. Neka {player} izabere drugog."}
|
"poruka": f"Nisam čuo ime lika. Neka {player} ponovi."}
|
||||||
if info["key"] in self.banned:
|
|
||||||
|
cached_key = self.store.resolve_alias(spoken) # samo ranije viđeni likovi
|
||||||
|
# brza provjera zabrane (pobjednici su prepoznati i keširani)
|
||||||
|
if cached_key and cached_key in self.banned:
|
||||||
|
name = (self.store.get(cached_key) or {}).get("name", spoken)
|
||||||
return {"ok": False, "razlog": "iskorišten",
|
return {"ok": False, "razlog": "iskorišten",
|
||||||
"poruka": f"{info['name']} je već pobijedio ranije u ovoj partiji i ne može se ponovo koristiti. "
|
"poruka": f"{name} je već pobijedio ranije u ovoj partiji i ne može se ponovo koristiti. "
|
||||||
f"Neka {player} izabere drugog heroja."}
|
f"Neka {player} izabere drugog heroja."}
|
||||||
if any(pk["key"] == info["key"] for pl, pk in self.picks.items() if pl != player):
|
# brza provjera duplikata u rundi
|
||||||
|
for pl, pk in self.picks.items():
|
||||||
|
if pl == player:
|
||||||
|
continue
|
||||||
|
if cached_key and pk.get("key") == cached_key:
|
||||||
return {"ok": False, "razlog": "duplikat",
|
return {"ok": False, "razlog": "duplikat",
|
||||||
"poruka": f"{info['name']} je već izabran u ovoj rundi. Neka {player} izabere drugog."}
|
"poruka": f"{pk['name']} je već izabran u ovoj rundi. Neka {player} izabere drugog."}
|
||||||
self.picks[player] = info
|
if not cached_key and normalize_key(pk.get("spoken", "")) == normalize_key(spoken):
|
||||||
return {"ok": True, "igrac": player, "heroj": info["name"], "poruka": f"{info['name']} je zabilježen za {player}."}
|
return {"ok": False, "razlog": "duplikat",
|
||||||
|
"poruka": f"Taj lik je već izabran u ovoj rundi. Neka {player} izabere drugog."}
|
||||||
|
|
||||||
|
if cached_key:
|
||||||
|
h = self.store.get(cached_key) or {}
|
||||||
|
pick = {"spoken": spoken, "key": cached_key, "name": h.get("name", spoken),
|
||||||
|
"desc": h.get("desc", ""), "resolved": True}
|
||||||
|
else:
|
||||||
|
pick = {"spoken": spoken, "key": "", "name": spoken.strip().title() or spoken,
|
||||||
|
"desc": "", "resolved": False}
|
||||||
|
self.picks[player] = pick
|
||||||
|
return {"ok": True, "igrac": player, "heroj": pick["name"],
|
||||||
|
"poruka": f"{pick['name']} je zabilježen za {player}."}
|
||||||
|
|
||||||
|
async def _resolve_pending(self) -> None:
|
||||||
|
"""Prepoznaj sve još neprepoznate likove u rundi (LLM + web search) — paralelno."""
|
||||||
|
pending = [(pl, pk) for pl, pk in self.picks.items() if not pk.get("resolved")]
|
||||||
|
if not pending:
|
||||||
|
return
|
||||||
|
async def _one(pl: str, pk: dict) -> None:
|
||||||
|
info = await resolve_hero(self.client, self.model, self.store, pk["spoken"])
|
||||||
|
if info:
|
||||||
|
pk["key"], pk["name"], pk["desc"] = info["key"], info["name"], info["desc"]
|
||||||
|
else:
|
||||||
|
# ni nakon pretrage nije prepoznat — koristi izgovoreno ime kao lik
|
||||||
|
pk["key"] = normalize_key(pk["spoken"]) or "nepoznato"
|
||||||
|
self.store.upsert(pk["key"], pk["name"], "", "", "web")
|
||||||
|
pk["resolved"] = True
|
||||||
|
await asyncio.gather(*[_one(pl, pk) for pl, pk in pending])
|
||||||
|
|
||||||
async def resolve_round(self) -> dict:
|
async def resolve_round(self) -> dict:
|
||||||
if not self.picks:
|
if not self.picks:
|
||||||
return {"ok": False, "poruka": "Još niko nije izabrao heroja u ovoj rundi."}
|
return {"ok": False, "poruka": "Još niko nije izabrao heroja u ovoj rundi."}
|
||||||
|
# tek sada prepoznaj likove (LLM + web search); potvrde su već izgovorene
|
||||||
|
await self._resolve_pending()
|
||||||
# heroji poredani po redoslijedu igranja
|
# heroji poredani po redoslijedu igranja
|
||||||
order = self.current_order
|
order = self.current_order
|
||||||
participants = [self.picks[p] for p in order if p in self.picks]
|
participants = [self.picks[p] for p in order if p in self.picks]
|
||||||
|
|||||||
Reference in New Issue
Block a user