Refaktor 1/4: data-driven registar alata umjesto elif lanaca

Uvodi Tool (idriz/tooling.py: shema + handler + mode inline/background).
Assistant gradi self.tools/self.dispatch iz TOOLS, a handle_function_call
generički pokreće alat po modu. Uklonjeni GAME_TOOLS set i dva elif lanca
(run_game_tool, run_tool) -> _run_inline / _run_background. Ponašanje isto.

Prvi korak ka plugin arhitekturi (mali app = jedan folder).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
idriz
2026-09-11 12:13:03 +02:00
parent 6839464161
commit 0e6d8947ce
2 changed files with 67 additions and 42 deletions

View File

@@ -12,6 +12,7 @@ from openai import AsyncOpenAI
from . import audio, coloring, game, printer, render, textgen, wiki from . import audio, coloring, game, printer, render, textgen, wiki
from .audio import Mic, Speaker from .audio import Mic, Speaker
from .config import Config from .config import Config
from .tooling import Tool
log = logging.getLogger("idriz.assistant") log = logging.getLogger("idriz.assistant")
@@ -123,9 +124,6 @@ TOOLS = [
}, },
] ]
GAME_TOOLS = {"superheroji_nova_partija", "superheroji_izbor", "superheroji_kraj"}
def _slug(s: str) -> str: def _slug(s: str) -> str:
s = re.sub(r"[^a-zA-Z0-9]+", "-", s).strip("-").lower() s = re.sub(r"[^a-zA-Z0-9]+", "-", s).strip("-").lower()
return s[:40] or "posao" return s[:40] or "posao"
@@ -145,6 +143,17 @@ class Assistant:
self.cancelled_responses: set[str] = set() self.cancelled_responses: set[str] = set()
self.match: game.Match | None = None 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 ---------- # ---------- alati ----------
async def tool_wikipedia(self, args: dict) -> str: async def tool_wikipedia(self, args: dict) -> str:
topic = args.get("tema") or args.get("tema_bosanski") or "" 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. Do sada vodi {st['pobjednici'][0]}. Poredak: {poredak}. Reci to djeci."
return f"Partija je prekinuta. 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: PLACEHOLDER = ("Posao je pokrenut i trajat će oko pola minute do minutu. "
"""Igra: izračunaj rezultat pa ga odmah vrati modelu da ga izgovori.""" "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: try:
if name == "superheroji_nova_partija": result = await tool.handler(args)
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}."
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
log.exception("igra: alat %s nije uspio", name) log.exception("alat %s nije uspio", tool.name)
result = f"Došlo je do greške u igri ({type(e).__name__}). Reci djeci da probamo ponovo." result = f"Došlo je do greške ({type(e).__name__}). Reci to djetetu kratko, na bosanskom."
try: try:
await self.conn.send({ await self._send_output(call_id, result)
"type": "conversation.item.create",
"item": {"type": "function_call_output", "call_id": call_id, "output": result},
})
await self.conn.send({"type": "response.create"}) await self.conn.send({"type": "response.create"})
except Exception: # noqa: BLE001 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: async def _run_background(self, tool: Tool, args: dict) -> None:
"""Pozadinski posao; kad završi, obavijesti model porukom i zatraži novi odgovor.""" """Dug posao; kad završi, obavijesti model porukom i zatraži novi odgovor."""
try: try:
if name == "wikipedia_sazetak": result = await tool.handler(args)
result = await self.tool_wikipedia(args)
elif name == "bojanka":
result = await self.tool_bojanka(args)
else:
result = f"Nepoznat alat {name}."
ok = True ok = True
except Exception as e: # noqa: BLE001 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}" result = f"Posao nije uspio: {type(e).__name__}: {e}"
ok = False ok = False
await self.notify_model(("GOTOVO. " if ok else "GREŠKA. ") + result + " Reci to djetetu kratko, na bosanskom.") 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: except json.JSONDecodeError:
args = {} args = {}
log.info("alat %s(%s)", name, args) log.info("alat %s(%s)", name, args)
if name in GAME_TOOLS: tool = self.dispatch.get(name)
# igra: brzo, rezultat ide odmah nazad modelu (bez "radim na tome") if tool is None:
task = asyncio.create_task(self.run_game_tool(name, call_id, args)) await self._send_output(call_id, f"Nepoznat alat {name}.")
self.jobs.add(task) await self.conn.send({"type": "response.create"})
task.add_done_callback(self.jobs.discard)
return 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) self.jobs.add(task)
task.add_done_callback(self.jobs.discard) 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 ---------- # ---------- audio petlje ----------
async def mic_loop(self) -> None: async def mic_loop(self) -> None:

24
idriz/tooling.py Normal file
View File

@@ -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"]