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 .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:

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