All checks were successful
Deploy / deploy (push) Successful in 3m43s
- 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: вывод в продакшен на мини-ПК
105 lines
3.6 KiB
Python
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()
|