voice: локальный агент (fast-path), tool control_light, смелая тема, RUNBOOK
All checks were successful
Deploy / deploy (push) Successful in 3m43s
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: вывод в продакшен на мини-ПК
This commit is contained in:
104
agent/predict.py
Normal file
104
agent/predict.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user