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:
@@ -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
24
idriz/tooling.py
Normal 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"]
|
||||||
Reference in New Issue
Block a user