Files
smart-home-tablet/agent/predict.py
d.klimov e6dab09c38
All checks were successful
Deploy / deploy (push) Successful in 3m43s
voice: локальный агент (fast-path), tool control_light, смелая тема, RUNBOOK
- lib/agent-local.ts + route.ts: команды-действия исполняет локальная intent-служба
  (под AGENT_URL, с fallback на LLM); инфо-запросы остаются у LLM
- lib/tools/lights.ts: голосовое управление светом через HA (control_light)
- app/globals.css + page.tsx: космическая indigo/aurora тема (токены, kiosk-safe)
- agent/: sidecar-служба (Dockerfile, predict.py), веса монтируются томом
- RUNBOOK.md: вывод в продакшен на мини-ПК
2026-07-22 21:48:24 +03:00

105 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Инференс joint-модели планшета: CLI и HTTP-служба.
Разовый разбор:
python predict.py "поставь таймер на 15 минут"
python predict.py "включи Ваню Дмитриенко"
HTTP-служба (для app/api/voice/chat/route.ts как локальный «мозг»):
python predict.py --serve --port 8091
# POST /predict {"text": "..."} → {intent, slots, score, tool}
# score < THRESHOLD → intent принудительно none (порог на стороне службы,
# но потребитель всё равно решает сам падать ли на облачный LLM).
Порог THRESHOLD=0.7 по умолчанию (переопределяется env).
"""
import argparse
import json
import os
import sys
import joint_model_def as jm
import tablet_map
HERE = os.path.dirname(os.path.abspath(__file__))
MODEL_DIR = os.path.join(HERE, "joint_model")
THRESHOLD = float(os.environ.get("THRESHOLD", "0.7"))
_model = _tok = _cfg = None
def _ensure_loaded():
global _model, _tok, _cfg
if _model is None:
_model, _tok, _cfg = jm.load(MODEL_DIR)
jm.predict(_model, _tok, _cfg, "прогрев") # первый форвард медленнее
def analyze(text):
_ensure_loaded()
intent, slots, score = jm.predict(_model, _tok, _cfg, text)
if score < THRESHOLD:
intent = "none"
tool = tablet_map.to_tool_call(intent, slots) if intent != "none" else None
return {"intent": intent, "slots": slots, "score": round(score, 3), "tool": tool}
def serve(port):
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def _send(self, code, obj):
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self._send(200, {"ok": True})
else:
self._send(404, {"error": "not found"})
def do_POST(self):
if self.path != "/predict":
return self._send(404, {"error": "not found"})
n = int(self.headers.get("Content-Length", 0))
try:
data = json.loads(self.rfile.read(n) or b"{}")
text = (data.get("text") or "").strip()
except Exception:
return self._send(400, {"error": "bad json"})
if not text:
return self._send(400, {"error": "empty text"})
self._send(200, analyze(text))
def log_message(self, *a):
pass # тихо
_ensure_loaded()
print(f"tablet-agent слушает http://127.0.0.1:{port} (порог {THRESHOLD})")
HTTPServer(("127.0.0.1", port), Handler).serve_forever()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("text", nargs="*", help="запрос для разбора")
ap.add_argument("--serve", action="store_true", help="поднять HTTP-службу")
ap.add_argument("--port", type=int, default=8091)
args = ap.parse_args()
if args.serve:
serve(args.port)
return
if not args.text:
print("Использование: python predict.py \"текст запроса\"")
sys.exit(1)
print(json.dumps(analyze(" ".join(args.text)), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()