diff --git a/admin/app.py b/admin/app.py index 96e5a71..01a0a0c 100644 --- a/admin/app.py +++ b/admin/app.py @@ -19,7 +19,7 @@ from flask import ( # Pristup skladištu igre (bez openai zavisnosti) iz glavnog idriz paketa. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from idriz import gamestore # noqa: E402 +from idriz.apps.superheroji import store as gamestore # noqa: E402 # --- Konfiguracija ----------------------------------------------------------- diff --git a/idriz/apps/__init__.py b/idriz/apps/__init__.py new file mode 100644 index 0000000..bd1423d --- /dev/null +++ b/idriz/apps/__init__.py @@ -0,0 +1,12 @@ +"""Registar mini-app-ova. + +load_apps() LIJENO uvozi manifeste, da proces koji nema teške zavisnosti (npr. +admin bez openai-a) može uvesti lagane dijelove app-a (npr. apps.superheroji.store) +bez povlačenja svega. +""" +from .base import AppContext, AppSpec, AppTool # noqa: F401 + + +def load_apps() -> list[AppSpec]: + from .superheroji import manifest as superheroji + return [superheroji.SPEC] diff --git a/idriz/apps/base.py b/idriz/apps/base.py new file mode 100644 index 0000000..5a50425 --- /dev/null +++ b/idriz/apps/base.py @@ -0,0 +1,65 @@ +"""Osnovni tipovi za mini-app-ove (plugine). + +Svaki app se opisuje jednim AppSpec-om: doprinos promptu, pozdravu, i listu alata. +AppContext je NAMJERNO uzak most prema runtime-u (Assistant-u) — samo par metoda, +da ne postane novi "god object". +""" +import asyncio +from dataclasses import dataclass, field +from typing import Awaitable, Callable + +# Handler app-alata: prima kontekst i argumente, vraća tekst za model. +AppHandler = Callable[["AppContext", dict], Awaitable[str]] + + +@dataclass +class AppTool: + schema: dict # OpenAI function schema (ima "name") + handler: AppHandler # async (ctx, args) -> str + mode: str = "inline" # "inline" (brzo) ili "background" (dug posao) + + +@dataclass +class AppSpec: + name: str + title: str + instructions: str = "" # doprinos sistemskom promptu + greeting: str = "" # kratka fraza za pozdrav (opciono) + tools: list[AppTool] = field(default_factory=list) + + +class AppContext: + """Most između app-a i runtime-a. Drži ga uskim (par metoda).""" + + def __init__(self, runtime) -> None: + self._rt = runtime + + @property + def client(self): + return self._rt.client + + @property + def cfg(self): + return self._rt.cfg + + # per-razgovor stanje app-a (npr. trenutna partija) + def get_state(self, key: str): + return self._rt.app_state.get(key) + + def set_state(self, key: str, value) -> None: + self._rt.app_state[key] = value + + # reci nešto modelu (kontekst-poruka + novi odgovor), pazi na jedan-odgovor-u-vremenu + async def notify(self, text: str) -> None: + await self._rt.notify_model(text) + + # kratko podstakni model (npr. filler "još razmišljam") + async def nudge(self, instructions: str) -> None: + await self._rt._nudge(instructions) + + # pokreni pozadinski posao (praćen, da se ne izgubi) + def background(self, coro) -> asyncio.Task: + task = asyncio.create_task(coro) + self._rt.jobs.add(task) + task.add_done_callback(self._rt.jobs.discard) + return task diff --git a/idriz/apps/superheroji/__init__.py b/idriz/apps/superheroji/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/idriz/game.py b/idriz/apps/superheroji/logic.py similarity index 99% rename from idriz/game.py rename to idriz/apps/superheroji/logic.py index eeb948d..584e5c3 100644 --- a/idriz/game.py +++ b/idriz/apps/superheroji/logic.py @@ -6,10 +6,11 @@ import logging from openai import AsyncOpenAI -from . import gamestore, wiki -from .gamestore import HeroStore, normalize_key +from idriz import wiki +from . import store as gamestore +from .store import HeroStore, normalize_key -log = logging.getLogger("idriz.game") +log = logging.getLogger("idriz.superheroji") RESOLVE_SYS = """Ti si stručnjak za izmišljene likove iz SVIH medija: stripovi, filmovi, crtani, video igre, \ knjige, mitologija, memovi. Dobiješ ime lika kako ga je izgovorilo dijete (može biti na bosanskom, sa greškom u \ diff --git a/idriz/apps/superheroji/manifest.py b/idriz/apps/superheroji/manifest.py new file mode 100644 index 0000000..89c9fd4 --- /dev/null +++ b/idriz/apps/superheroji/manifest.py @@ -0,0 +1,174 @@ +"""Mini-app 'Superheroji': doprinos promptu, alati i handleri. + +Sva logika partije je u logic.py; ovdje je samo spoj na runtime preko AppContext-a. +""" +import asyncio +import logging + +from ..base import AppContext, AppSpec, AppTool +from . import logic + +log = logging.getLogger("idriz.superheroji") + +STATE = "superheroji_match" # ključ per-razgovor stanja (trenutna partija) + +INSTRUCTIONS = """IGRA "SUPERHEROJI" (glasom, bez štampanja). Alati: superheroji_nova_partija, superheroji_izbor, superheroji_kraj. +Kako se igra Superheroji: +- NE objašnjavaj pravila na početku. Pretpostavi da djeca znaju igrati. Pravila objasni samo ako te izričito pitaju. +- Kad djeca kažu da žele igrati: prvo pitaj koliko igrača igra (od 2 do 5, mora biti najmanje dvoje), pa ih pitaj za imena. Onda pozovi superheroji_nova_partija sa listom imena. Alat ti vrati broj rundi i redoslijed biranja za prvu rundu. +- U svakoj rundi traži od igrača, TAČNO onim redoslijedom koji ti je alat dao, da svaki kaže jednog lika. Definicija je široka: bilo koji izmišljeni lik iz bilo kog medija — ne samo klasični superheroji. To su i crtani likovi, likovi iz video igara, animea, filmova, knjiga, memova (npr. Kuromi, Pikaču, Goku, La Linea, Mario, Gandalf, Baba Jaga, Mad Max). Zadnji igrač ima prednost jer bira zadnji, zato se redoslijed mijenja svaku rundu — uvijek koristi redoslijed koji ti alat da. +- VRLO VAŽNO ZA IMENA: djeca često biraju likove neobičnih ili stranih imena. Slušaj TAČNO ime koje dijete kaže i NE zamjenjuj ga poznatim superherojem koji zvuči slično (ako dijete kaže "Kuromi", ne smiješ čuti "Hulk"). Ako ime nisi jasno čuo ili ga ne razumiješ, NE POGAĐAJ — reci "Nisam dobro čuo ime, možeš li ponoviti?" i sačekaj. Tek kad si siguran u ime, pozovi alat. +- Čim igrač jasno kaže lika, pozovi superheroji_izbor (igrac, heroj) sa imenom TAČNO kako ga je dijete reklo. Alat ti onda kaže šta dalje. UVIJEK prvo naglas potvrdiš ime tako što ga izgovoriš ("Kuromi, zabilježeno!"), da dijete čuje jesi li dobro razumio — to radiš za SVAKOG igrača, i za zadnjeg u rundi. +- Ako dijete kaže da si pogriješio ime (npr. "nisam rekao Hulk, rekao sam Kuromi"), ODMAH ponovo pozovi superheroji_izbor sa ispravnim imenom za istog igrača. Ne prelazi dalje dok ime nije tačno. +- Ako alat odbije izbor (lik je već pobijedio ranije, ili je već izabran u rundi), kratko reci zašto i traži od tog igrača drugog lika. +- Kad ZADNJI igrač u rundi izabere, presuda kreće SAMA — ne zoveš nikakav alat za nju. Ti tada samo: (1) potvrdiš ime zadnjeg lika naglas i (2) veselo kažeš da sada gledaš ko pobjeđuje ("Da vidim ko je jači..."). Zatim SAČEKAJ — rezultat runde ću ti ja poslati porukom, pa ga tek onda objavi djeci: ko je pobijedio i zašto, veselo. Pobjednički lik se više NE može birati do kraja partije. +- Poruku sa rezultatom (počinje sa REZULTAT RUNDE) obavezno objavi djeci. Ona ti kaže i da li slijedi nova runda (s kojim redoslijedom) ili je partija gotova. Kad je gotova, objavi konačnog pobjednika partije i čestitaj. +- Ako djeca žele stati ranije, pozovi superheroji_kraj. +- Za igru ne treba potvrda kao za štampanje — samo lijepo vodi igru.""" + +NOVA_SCHEMA = { + "type": "function", + "name": "superheroji_nova_partija", + "description": "Započinje novu partiju igre Superheroji. Pozvati NAKON što si pitao koliko igrača (2-5) i njihova imena. Vraća broj rundi i redoslijed biranja za prvu rundu.", + "parameters": { + "type": "object", + "properties": { + "igraci": {"type": "array", "items": {"type": "string"}, "description": "Imena igrača redom, od 2 do 5 (najmanje dvoje)."}, + }, + "required": ["igraci"], + }, +} + +IZBOR_SCHEMA = { + "type": "function", + "name": "superheroji_izbor", + "description": "Prijavljuje heroja kojeg je jedan igrač izabrao u trenutnoj rundi. Pozvati čim igrač kaže ime lika. Vraća potvrdu ili razlog odbijanja.", + "parameters": { + "type": "object", + "properties": { + "igrac": {"type": "string", "description": "Ime igrača koji bira."}, + "heroj": {"type": "string", "description": "Ime lika kako ga je igrač rekao."}, + }, + "required": ["igrac", "heroj"], + }, +} + +KRAJ_SCHEMA = { + "type": "function", + "name": "superheroji_kraj", + "description": "Prekida trenutnu partiju ranije ako igrači žele stati. Vraća trenutni rezultat.", + "parameters": {"type": "object", "properties": {}}, +} + +_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 nova(ctx: AppContext, args: dict) -> str: + players = args.get("igraci") or [] + if isinstance(players, str): + players = [players] + players = [p.strip() for p in players if p and p.strip()] + if len(players) < 2: + return ("Za igru Superheroji treba najmanje dvoje igrača. Reci djeci da moraju biti bar dvoje, " + "pitaj koliko će igrati (2 do 5) i njihova imena, pa ponovo pozovi superheroji_nova_partija.") + match = logic.Match(ctx.client, ctx.cfg.game_model, players) + ctx.set_state(STATE, match) + info = match.start_info() + order = ", ".join(info["redoslijed"]) + prvi = info["redoslijed"][0] + return (f"Partija je počela. Igrači: {', '.join(info['igraci'])}. Broj rundi: {info['broj_rundi']}. " + f"RUNDA 1 — redoslijed biranja: {order}. " + f"Traži od igrača {prvi} da kaže svog superheroja, pa redom ostale tim redoslijedom. " + f"Kad ti igrač kaže heroja, pozovi superheroji_izbor.") + + +async def izbor(ctx: AppContext, args: dict) -> str: + match = ctx.get_state(STATE) + if not match: + return "Nema aktivne partije. Prvo pokreni igru sa superheroji_nova_partija." + res = await match.record_pick(args.get("igrac", ""), args.get("heroj", "")) + if not res.get("ok"): + return (res.get("poruka", "Taj izbor ne može proći.") + + " Kad ti dijete kaže drugog lika, ponovo pozovi superheroji_izbor.") + name = res["heroj"] + confirm = (f'OBAVEZNO prvo reci djetetu tačno ovo ime naglas: "{name}, zabilježeno!" — koristi baš to ime ' + f'da dijete čuje kako si ga razumio. Ako dijete kaže da to nije taj lik, ponovo pozovi ' + f'superheroji_izbor sa ispravnim imenom za istog igrača.') + if match.round_complete: + ctx.background(_adjudicate_and_announce(ctx, match)) + return (confirm + ' Svi su izabrali. Zatim veselo reci da SADA gledaš ko pobjeđuje ' + '(npr. "Da vidim ko je jači, samo tren..."). NE zovi nijedan alat — ' + 'rezultat runde ću ti poslati porukom, pa ga onda objavi.') + nxt = match.next_picker() + return confirm + f" Zatim pitaj igrača {nxt} da kaže svog lika." + + +async def kraj(ctx: AppContext, args: dict) -> str: + match = ctx.get_state(STATE) + if not match: + return "Trenutno nema aktivne partije." + st = match.standings() + poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"]) + ctx.set_state(STATE, None) + if st["pobjednici"] and not st["nerijeseno"]: + return f"Partija je prekinuta. Do sada vodi {st['pobjednici'][0]}. Poredak: {poredak}. Reci to djeci." + return f"Partija je prekinuta. Poredak: {poredak}. Reci to djeci." + + +async def _adjudicate_and_announce(ctx: AppContext, match) -> None: + """Presudi rundu (može potrajati zbog prepoznavanja/web searcha). Ubacuj filler dok traje.""" + adj = asyncio.create_task(match.resolve_round()) + i = 0 + try: + while True: + try: + res = await asyncio.wait_for(asyncio.shield(adj), timeout=9) + break + except asyncio.TimeoutError: + await ctx.nudge(_FILLERS[min(i, len(_FILLERS) - 1)]) + i += 1 + except Exception: # noqa: BLE001 + log.exception("presuda runde nije uspjela") + await ctx.notify("Došlo je do greške pri presudi runde. Reci djeci da još jednom kažu svoje likove.") + return + await ctx.notify(_format_verdict(ctx, res)) + + +def _format_verdict(ctx: AppContext, res: dict) -> str: + if not res.get("ok"): + return res.get("poruka", "Runda još nije spremna za presudu.") + txt = (f"REZULTAT RUNDE {res['runda']}: pobjeđuje {res['pobjednik_heroj']} " + f"(izabrao ga {res['pobjednik_igrac']}). Obrazloženje: {res['obrazlozenje']} " + f"Objavi ovo djeci veselo — ko je pobijedio i zašto. " + f"Napomeni da se {res['pobjednik_heroj']} više ne može birati u ovoj partiji.") + if res.get("gotovo"): + st = res["konacni_rezultat"] + poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"]) + if st["nerijeseno"]: + txt += f" PARTIJA JE GOTOVA! Neriješeno je (poredak: {poredak}). Čestitaj svima." + else: + txt += f" PARTIJA JE GOTOVA! Pobjednik partije je {st['pobjednici'][0]}! Poredak: {poredak}. Čestitaj pobjedniku." + ctx.set_state(STATE, None) + else: + order = ", ".join(res["sljedeci_redoslijed"]) + prvi = res["sljedeci_redoslijed"][0] + txt += (f" Zatim kreće RUNDA {res['sljedeca_runda']} — redoslijed: {order}. " + f"Traži od igrača {prvi} novog heroja.") + return txt + + +SPEC = AppSpec( + name="superheroji", + title="Superheroji", + instructions=INSTRUCTIONS, + greeting="možete zajedno igrati igru Superheroji", + tools=[ + AppTool(NOVA_SCHEMA, nova, "inline"), + AppTool(IZBOR_SCHEMA, izbor, "inline"), + AppTool(KRAJ_SCHEMA, kraj, "inline"), + ], +) diff --git a/idriz/gamestore.py b/idriz/apps/superheroji/store.py similarity index 100% rename from idriz/gamestore.py rename to idriz/apps/superheroji/store.py diff --git a/idriz/assistant.py b/idriz/assistant.py index 13d9b7e..132d127 100644 --- a/idriz/assistant.py +++ b/idriz/assistant.py @@ -9,7 +9,9 @@ from pathlib import Path from openai import AsyncOpenAI -from . import audio, coloring, game, printer, render, textgen, wiki +from . import audio, coloring, printer, render, textgen, wiki +from .apps import load_apps +from .apps.base import AppContext from .audio import Mic, Speaker from .config import Config from .tooling import Tool @@ -24,20 +26,7 @@ Govoriš toplo, kratko i jasno, kao strpljivi stariji brat. Rečenice su kratke. 1. RAZGOVARATI I ODGOVARATI NA PITANJA. Dijete te može pitati bilo šta: za školu, o svijetu, o životinjama, svemiru, historiji, matematici, riječima... Odgovaraš iz svog znanja, glasom, bez štampanja. 2. Odštampati sažetak o nekoj temi. Tražiš članak na engleskoj Wikipediji, prevedeš ga na bosanski i odštampaš sažetak do dvije stranice. Alat: wikipedia_sazetak. 3. Odštampati bojanku: crno-bijelu sliku sa konturama koju dijete može bojiti. Alat: bojanka. -4. IGRATI IGRU "SUPERHEROJI" (glasom, bez štampanja). Alati: superheroji_nova_partija, superheroji_izbor, superheroji_kraj. - -Kako se igra Superheroji: -- NE objašnjavaj pravila na početku. Pretpostavi da djeca znaju igrati. Pravila objasni samo ako te izričito pitaju. -- Kad djeca kažu da žele igrati: prvo pitaj koliko igrača igra (od 2 do 5, mora biti najmanje dvoje), pa ih pitaj za imena. Onda pozovi superheroji_nova_partija sa listom imena. Alat ti vrati broj rundi i redoslijed biranja za prvu rundu. -- U svakoj rundi traži od igrača, TAČNO onim redoslijedom koji ti je alat dao, da svaki kaže jednog lika. Definicija je široka: bilo koji izmišljeni lik iz bilo kog medija — ne samo klasični superheroji. To su i crtani likovi, likovi iz video igara, animea, filmova, knjiga, memova (npr. Kuromi, Pikaču, Goku, La Linea, Mario, Gandalf, Baba Jaga, Mad Max). Zadnji igrač ima prednost jer bira zadnji, zato se redoslijed mijenja svaku rundu — uvijek koristi redoslijed koji ti alat da. -- VRLO VAŽNO ZA IMENA: djeca često biraju likove neobičnih ili stranih imena. Slušaj TAČNO ime koje dijete kaže i NE zamjenjuj ga poznatim superherojem koji zvuči slično (ako dijete kaže "Kuromi", ne smiješ čuti "Hulk"). Ako ime nisi jasno čuo ili ga ne razumiješ, NE POGAĐAJ — reci "Nisam dobro čuo ime, možeš li ponoviti?" i sačekaj. Tek kad si siguran u ime, pozovi alat. -- Čim igrač jasno kaže lika, pozovi superheroji_izbor (igrac, heroj) sa imenom TAČNO kako ga je dijete reklo. Alat ti onda kaže šta dalje. UVIJEK prvo naglas potvrdiš ime tako što ga izgovoriš ("Kuromi, zabilježeno!"), da dijete čuje jesi li dobro razumio — to radiš za SVAKOG igrača, i za zadnjeg u rundi. -- Ako dijete kaže da si pogriješio ime (npr. "nisam rekao Hulk, rekao sam Kuromi"), ODMAH ponovo pozovi superheroji_izbor sa ispravnim imenom za istog igrača. Ne prelazi dalje dok ime nije tačno. -- Ako alat odbije izbor (lik je već pobijedio ranije, ili je već izabran u rundi), kratko reci zašto i traži od tog igrača drugog lika. -- Kad ZADNJI igrač u rundi izabere, presuda kreće SAMA — ne zoveš nikakav alat za nju. Ti tada samo: (1) potvrdiš ime zadnjeg lika naglas i (2) veselo kažeš da sada gledaš ko pobjeđuje ("Da vidim ko je jači..."). Zatim SAČEKAJ — rezultat runde ću ti ja poslati porukom, pa ga tek onda objavi djeci: ko je pobijedio i zašto, veselo. Pobjednički lik se više NE može birati do kraja partije. -- Poruku sa rezultatom (počinje sa REZULTAT RUNDE) obavezno objavi djeci. Ona ti kaže i da li slijedi nova runda (s kojim redoslijedom) ili je partija gotova. Kad je gotova, objavi konačnog pobjednika partije i čestitaj. -- Ako djeca žele stati ranije, pozovi superheroji_kraj. -- Za igru ne treba potvrda kao za štampanje — samo lijepo vodi igru. +Osim toga, možeš voditi i posebne igre i aktivnosti opisane niže (samo ako ih dijete zatraži). Kako odgovaraš na pitanja: - Prvi odgovor je kratak: dvije do četiri rečenice, najvažnije prvo. Zatim možeš pitati "Hoćeš da ti kažem još?" ili "Je li ti jasno?". @@ -91,37 +80,6 @@ TOOLS = [ "required": ["opis", "opis_bosanski"], }, }, - { - "type": "function", - "name": "superheroji_nova_partija", - "description": "Započinje novu partiju igre Superheroji. Pozvati NAKON što si pitao koliko igrača (2-5) i njihova imena. Vraća broj rundi i redoslijed biranja za prvu rundu.", - "parameters": { - "type": "object", - "properties": { - "igraci": {"type": "array", "items": {"type": "string"}, "description": "Imena igrača redom, od 2 do 5 (najmanje dvoje)."}, - }, - "required": ["igraci"], - }, - }, - { - "type": "function", - "name": "superheroji_izbor", - "description": "Prijavljuje heroja kojeg je jedan igrač izabrao u trenutnoj rundi. Pozvati čim igrač kaže ime lika. Vraća potvrdu ili razlog odbijanja.", - "parameters": { - "type": "object", - "properties": { - "igrac": {"type": "string", "description": "Ime igrača koji bira."}, - "heroj": {"type": "string", "description": "Ime lika kako ga je igrač rekao."}, - }, - "required": ["igrac", "heroj"], - }, - }, - { - "type": "function", - "name": "superheroji_kraj", - "description": "Prekida trenutnu partiju ranije ako igrači žele stati. Vraća trenutni rezultat.", - "parameters": {"type": "object", "properties": {}}, - }, ] def _slug(s: str) -> str: @@ -141,19 +99,38 @@ class Assistant: self.current_response = "" self.response_active = False self.cancelled_responses: set[str] = set() - self.match: game.Match | None = None + self.app_state: dict = {} # per-razgovor stanje mini-app-ova + + # --- Sastavljanje iz jezgra + mini-app-ova (plugina) --- + self.ctx = AppContext(self) + self.apps = load_apps() - # Registar alata: shema + handler + način izvršavanja (umjesto elif lanaca). _schema = {t["name"]: t for t in TOOLS} self.tools: list[Tool] = [ Tool(_schema["wikipedia_sazetak"], self.tool_wikipedia, "background"), Tool(_schema["bojanka"], self.tool_bojanka, "background"), - Tool(_schema["superheroji_nova_partija"], self.game_nova, "inline"), - Tool(_schema["superheroji_izbor"], self.game_izbor, "inline"), - Tool(_schema["superheroji_kraj"], self.game_kraj, "inline"), ] + for app in self.apps: + for at in app.tools: + self.tools.append(Tool(at.schema, self._bind(at.handler), at.mode)) self.dispatch: dict[str, Tool] = {t.name: t for t in self.tools} + # prompt = jezgro + doprinosi app-ova; pozdrav = jezgro + fraze app-ova + self.instructions = INSTRUCTIONS + "".join( + "\n\n" + app.instructions for app in self.apps if app.instructions) + self.tool_schemas = [t.schema for t in self.tools] + greetings = "".join(f" {app.greeting}." for app in self.apps if app.greeting) + self.greeting_instr = ( + "Kratko se predstavi na bosanskom u jednoj ili dvije rečenice: ti si Idriz, " + "dijete te može nešto pitati, a možeš i odštampati sažetak o nekoj temi ili bojanku." + + greetings) + + def _bind(self, handler): + """Veže AppContext za app-handler (ctx, args) -> handler(args) za registar.""" + async def _run(args: dict) -> str: + return await handler(self.ctx, args) + return _run + # ---------- alati ---------- async def tool_wikipedia(self, args: dict) -> str: topic = args.get("tema") or args.get("tema_bosanski") or "" @@ -201,103 +178,6 @@ class Assistant: await printer.print_file(out, self.cfg.printer, self.cfg.dry_run, fit=True) return f"Bojanka '{subject_bs}' je odštampana." - # ---------- igra Superheroji ---------- - async def game_nova(self, args: dict) -> str: - players = args.get("igraci") or [] - if isinstance(players, str): - players = [players] - players = [p.strip() for p in players if p and p.strip()] - if len(players) < 2: - return ("Za igru Superheroji treba najmanje dvoje igrača. Reci djeci da moraju biti bar dvoje, " - "pitaj koliko će igrati (2 do 5) i njihova imena, pa ponovo pozovi superheroji_nova_partija.") - self.match = game.Match(self.client, self.cfg.game_model, players) - info = self.match.start_info() - order = ", ".join(info["redoslijed"]) - prvi = info["redoslijed"][0] - return (f"Partija je počela. Igrači: {', '.join(info['igraci'])}. Broj rundi: {info['broj_rundi']}. " - f"RUNDA 1 — redoslijed biranja: {order}. " - f"Traži od igrača {prvi} da kaže svog superheroja, pa redom ostale tim redoslijedom. " - f"Kad ti igrač kaže heroja, pozovi superheroji_izbor.") - - async def game_izbor(self, args: dict) -> str: - if not self.match: - return "Nema aktivne partije. Prvo pokreni igru sa superheroji_nova_partija." - res = await self.match.record_pick(args.get("igrac", ""), args.get("heroj", "")) - if not res.get("ok"): - return (res.get("poruka", "Taj izbor ne može proći.") + - " Kad ti dijete kaže drugog lika, ponovo pozovi superheroji_izbor.") - name = res["heroj"] - # UVIJEK prvo potvrdi ime (i za zadnjeg igrača), da dijete čuje jesi li dobro razumio - confirm = (f'OBAVEZNO prvo reci djetetu tačno ovo ime naglas: "{name}, zabilježeno!" — koristi baš to ime ' - f'da dijete čuje kako si ga razumio. Ako dijete kaže da to nije taj lik, ponovo pozovi ' - f'superheroji_izbor sa ispravnim imenom za istog igrača.') - if self.match.round_complete: - # zadnji igrač je izabrao — presuda kreće sama u pozadini - task = asyncio.create_task(self._adjudicate_and_announce(self.match)) - self.jobs.add(task) - task.add_done_callback(self.jobs.discard) - return (confirm + ' Svi su izabrali. Zatim veselo reci da SADA gledaš ko pobjeđuje ' - '(npr. "Da vidim ko je jači, samo tren..."). NE zovi nijedan alat — ' - 'rezultat runde ću ti poslati porukom, pa ga onda objavi.') - nxt = self.match.next_picker() - 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: - """Presudi rundu (može potrajati zbog prepoznavanja/web searcha). Ubacuj filler dok traje.""" - adj = asyncio.create_task(match.resolve_round()) - i = 0 - try: - while True: - try: - res = await asyncio.wait_for(asyncio.shield(adj), timeout=9) - break - except asyncio.TimeoutError: - await self._nudge(self._FILLERS[min(i, len(self._FILLERS) - 1)]) - i += 1 - except Exception: # noqa: BLE001 - 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.") - return - await self.notify_model(self._format_verdict(res)) - - def _format_verdict(self, res: dict) -> str: - if not res.get("ok"): - return res.get("poruka", "Runda još nije spremna za presudu.") - txt = (f"REZULTAT RUNDE {res['runda']}: pobjeđuje {res['pobjednik_heroj']} " - f"(izabrao ga {res['pobjednik_igrac']}). Obrazloženje: {res['obrazlozenje']} " - f"Objavi ovo djeci veselo — ko je pobijedio i zašto. " - f"Napomeni da se {res['pobjednik_heroj']} više ne može birati u ovoj partiji.") - if res.get("gotovo"): - st = res["konacni_rezultat"] - poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"]) - if st["nerijeseno"]: - txt += f" PARTIJA JE GOTOVA! Neriješeno je (poredak: {poredak}). Čestitaj svima." - else: - txt += f" PARTIJA JE GOTOVA! Pobjednik partije je {st['pobjednici'][0]}! Poredak: {poredak}. Čestitaj pobjedniku." - self.match = None - else: - order = ", ".join(res["sljedeci_redoslijed"]) - prvi = res["sljedeci_redoslijed"][0] - txt += (f" Zatim kreće RUNDA {res['sljedeca_runda']} — redoslijed: {order}. " - f"Traži od igrača {prvi} novog heroja.") - return txt - - async def game_kraj(self, args: dict) -> str: - if not self.match: - return "Trenutno nema aktivne partije." - st = self.match.standings() - poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"]) - self.match = None - if st["pobjednici"] and not st["nerijeseno"]: - return f"Partija je prekinuta. Do sada vodi {st['pobjednici'][0]}. Poredak: {poredak}. Reci to djeci." - return f"Partija je prekinuta. Poredak: {poredak}. Reci to djeci." - PLACEHOLDER = ("Posao je pokrenut i trajat će oko pola minute do minutu. " "Reci djetetu da sačeka; javit ću kad bude gotovo.") @@ -481,16 +361,16 @@ class Assistant: if not self.greeted: self.greeted = True await self.conn.send({"type": "response.create", "response": { - "instructions": "Kratko se predstavi na bosanskom u jednoj ili dvije rečenice: ti si Idriz, dijete te može nešto pitati, a možeš i odštampati sažetak o nekoj temi ili bojanku."}}) + "instructions": self.greeting_instr}}) async def session(self) -> None: async with self.client.realtime.connect(model=self.cfg.realtime_model) as conn: self.conn = conn await conn.session.update(session={ "type": "realtime", - "instructions": INSTRUCTIONS, + "instructions": self.instructions, "output_modalities": ["audio"], - "tools": TOOLS, + "tools": self.tool_schemas, "tool_choice": "auto", "audio": { "input": {