diff --git a/idriz/assistant.py b/idriz/assistant.py index 76bb6ef..13d9b7e 100644 --- a/idriz/assistant.py +++ b/idriz/assistant.py @@ -12,6 +12,7 @@ from openai import AsyncOpenAI from . import audio, coloring, game, printer, render, textgen, wiki from .audio import Mic, Speaker from .config import Config +from .tooling import Tool log = logging.getLogger("idriz.assistant") @@ -123,9 +124,6 @@ TOOLS = [ }, ] -GAME_TOOLS = {"superheroji_nova_partija", "superheroji_izbor", "superheroji_kraj"} - - def _slug(s: str) -> str: s = re.sub(r"[^a-zA-Z0-9]+", "-", s).strip("-").lower() return s[:40] or "posao" @@ -145,6 +143,17 @@ class Assistant: self.cancelled_responses: set[str] = set() self.match: game.Match | None = None + # 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"), + ] + self.dispatch: dict[str, Tool] = {t.name: t for t in self.tools} + # ---------- alati ---------- async def tool_wikipedia(self, args: dict) -> str: topic = args.get("tema") or args.get("tema_bosanski") or "" @@ -289,41 +298,35 @@ class Assistant: 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 run_game_tool(self, name: str, call_id: str, args: dict) -> None: - """Igra: izračunaj rezultat pa ga odmah vrati modelu da ga izgovori.""" + PLACEHOLDER = ("Posao je pokrenut i trajat će oko pola minute do minutu. " + "Reci djetetu da sačeka; javit ću kad bude gotovo.") + + async def _send_output(self, call_id: str, output: str) -> None: + await self.conn.send({ + "type": "conversation.item.create", + "item": {"type": "function_call_output", "call_id": call_id, "output": output}, + }) + + async def _run_inline(self, tool: Tool, call_id: str, args: dict) -> None: + """Brzi alat: rezultat handlera ide odmah nazad modelu.""" try: - if name == "superheroji_nova_partija": - result = await self.game_nova(args) - elif name == "superheroji_izbor": - result = await self.game_izbor(args) - elif name == "superheroji_kraj": - result = await self.game_kraj(args) - else: - result = f"Nepoznat alat {name}." + result = await tool.handler(args) except Exception as e: # noqa: BLE001 - log.exception("igra: alat %s nije uspio", name) - result = f"Došlo je do greške u igri ({type(e).__name__}). Reci djeci da probamo ponovo." + log.exception("alat %s nije uspio", tool.name) + result = f"Došlo je do greške ({type(e).__name__}). Reci to djetetu kratko, na bosanskom." try: - await self.conn.send({ - "type": "conversation.item.create", - "item": {"type": "function_call_output", "call_id": call_id, "output": result}, - }) + await self._send_output(call_id, result) await self.conn.send({"type": "response.create"}) except Exception: # noqa: BLE001 - log.exception("nisam uspio vratiti rezultat igre modelu") + log.exception("nisam uspio vratiti rezultat alata %s modelu", tool.name) - async def run_tool(self, name: str, call_id: str, args: dict) -> None: - """Pozadinski posao; kad završi, obavijesti model porukom i zatraži novi odgovor.""" + async def _run_background(self, tool: Tool, args: dict) -> None: + """Dug posao; kad završi, obavijesti model porukom i zatraži novi odgovor.""" try: - if name == "wikipedia_sazetak": - result = await self.tool_wikipedia(args) - elif name == "bojanka": - result = await self.tool_bojanka(args) - else: - result = f"Nepoznat alat {name}." + result = await tool.handler(args) ok = True except Exception as e: # noqa: BLE001 - log.exception("alat %s nije uspio", name) + log.exception("alat %s nije uspio", tool.name) result = f"Posao nije uspio: {type(e).__name__}: {e}" ok = False await self.notify_model(("GOTOVO. " if ok else "GREŠKA. ") + result + " Reci to djetetu kratko, na bosanskom.") @@ -389,22 +392,20 @@ class Assistant: except json.JSONDecodeError: args = {} log.info("alat %s(%s)", name, args) - if name in GAME_TOOLS: - # igra: brzo, rezultat ide odmah nazad modelu (bez "radim na tome") - task = asyncio.create_task(self.run_game_tool(name, call_id, args)) - self.jobs.add(task) - task.add_done_callback(self.jobs.discard) + tool = self.dispatch.get(name) + if tool is None: + await self._send_output(call_id, f"Nepoznat alat {name}.") + await self.conn.send({"type": "response.create"}) return - task = asyncio.create_task(self.run_tool(name, call_id, args)) + if tool.mode == "background": + # odmah vrati "radim na tome", pa posao ide u pozadini + await self._send_output(call_id, self.PLACEHOLDER) + await self.conn.send({"type": "response.create"}) + task = asyncio.create_task(self._run_background(tool, args)) + else: + task = asyncio.create_task(self._run_inline(tool, call_id, args)) self.jobs.add(task) task.add_done_callback(self.jobs.discard) - # odmah vrati rezultat da model može reći "radim na tome" - await self.conn.send({ - "type": "conversation.item.create", - "item": {"type": "function_call_output", "call_id": call_id, - "output": "Posao je pokrenut i trajat će oko pola minute do minutu. Reci djetetu da sačeka; javit ću kad bude gotovo."}, - }) - await self.conn.send({"type": "response.create"}) # ---------- audio petlje ---------- async def mic_loop(self) -> None: diff --git a/idriz/tooling.py b/idriz/tooling.py new file mode 100644 index 0000000..e0a27ed --- /dev/null +++ b/idriz/tooling.py @@ -0,0 +1,24 @@ +"""Zajednički tipovi za registraciju alata (koraci ka plugin arhitekturi).""" +from dataclasses import dataclass +from typing import Awaitable, Callable + +# Handler prima argumente alata i vraća tekst koji ide modelu. +Handler = Callable[[dict], Awaitable[str]] + + +@dataclass +class Tool: + """Jedan alat: shema (OpenAI function schema), handler i način izvršavanja. + + mode: + - "inline" — brzo; rezultat handlera ide odmah nazad modelu (npr. igra). + - "background" — dug posao; modelu se odmah kaže "radim na tome", a kad + handler završi, rezultat stiže preko notify_model (npr. štampanje). + """ + schema: dict + handler: Handler + mode: str = "inline" + + @property + def name(self) -> str: + return self.schema["name"]