Files
smart-home-tablet/agent/predict.py
Cosmo d71320a5a9
All checks were successful
Deploy / deploy (push) Successful in 1m58s
fix(voice): агент слушает 0.0.0.0 + защита tool_use.input от null
- agent/predict.py: HTTPServer bind 127.0.0.1 -> 0.0.0.0, иначе route.ts
  (контейнер планшета) не достукивается до tablet-agent по имени в сети coolify
- route.ts: historyToAnthropicMessages приводит не-объект/null из
  tc.function.arguments к {} — Anthropic 400 (tool_use.input must be object)
  на legacy-истории от Groq (arguments: "null")

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYsVrPyJ5phJeVsAGciS1v
2026-07-22 20:54:45 +00: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://0.0.0.0:{port} (порог {THRESHOLD})")
HTTPServer(("0.0.0.0", 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()