Files
smart-home-tablet/agent/tablet_map.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

156 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Карта: интент joint-модели → вызов tool планшета (smart-home-tablet).
Модель отдаёт (intent, slots) — «что сделать» и «куски текста параметров».
Приведение к боевому tool-call (направление, действие, режим в enum; «15 минут»
→ секунды; «Дани» → daniil) — здесь, на стороне потребителя. Модель конвенций
приложения не знает, она видела только текст запроса.
Это референс для интеграции в app/api/voice/chat/route.ts. На проде логика
переписывается на TypeScript, но соответствие интент→tool берётся отсюда.
"""
import re
# intent → (tool_name, фиксированные аргументы). Слоты домешиваются отдельно.
INTENT_TO_TOOL = {
"weather_get": ("get_weather", {}),
"transport_to_center": ("get_transport", {"direction": "to_center"}),
"transport_from_center": ("get_transport", {"direction": "from_center"}),
"transport_all": ("get_transport", {"direction": "all"}),
"calendar_today": ("get_today_events", {"range": "today"}),
"calendar_week": ("get_today_events", {"range": "week"}),
"calendar_create": ("create_event", {}),
"timer_set": ("set_timer", {}),
"timer_add": ("adjust_timer", {"sign": +1}),
"timer_subtract": ("adjust_timer", {"sign": -1}),
"timer_cancel": ("cancel_timer", {}),
"notes_get": ("get_notes", {}),
"home_state": ("get_smart_home_state", {}),
"purifier_on": ("control_air_purifier", {"action": "turn_on"}),
"purifier_off": ("control_air_purifier", {"action": "turn_off"}),
"purifier_mode": ("control_air_purifier", {"action": "set_mode"}),
"light_on": ("control_light", {"action": "turn_on"}), # НОВЫЙ tool
"light_off": ("control_light", {"action": "turn_off"}), # НОВЫЙ tool
"music_play": ("control_spotify", {"action": "play"}),
"music_pause": ("control_spotify", {"action": "pause"}),
"music_resume": ("control_spotify", {"action": "play"}),
"music_next": ("control_spotify", {"action": "next"}),
"music_prev": ("control_spotify", {"action": "previous"}),
"music_volume": ("control_spotify", {"action": "volume"}),
"now_playing": ("get_now_playing", {}),
}
OWNER_MAP = {
"дани": "daniil", "даниил": "daniil", "даниила": "daniil",
"даниилу": "daniil", "даня": "daniil", "мне": "daniil", "мой": "daniil",
"света": "sveta", "свете": "sveta", "светы": "sveta", "свету": "sveta",
}
MODE_MAP = {
"авто": "Auto", "автоматический": "Auto",
"ночной": "Night", "ночь": "Night",
"высокий": "High", "максимальный": "High", "турбо": "High",
"максимум": "High", "тихий": "Night", "низкий": "Night",
}
_NUM_WORDS = {
"полминуты": 30, "полчаса": 1800, "минуту": 60, "минуты": 60, "минут": 60,
"полторы": 90, "полтора": 5400, "пару": 120, "несколько": 180,
}
def parse_duration_seconds(text):
"""«15 минут» → 900, «полтора часа» → 5400, «30 секунд» → 30.
Грубый разбор для set/adjust таймера. На проде можно взять готовую
JS-либу разбора длительности; здесь — референс.
"""
t = text.lower().strip()
if t in ("полчаса",):
return 1800
if t in ("полминуты",):
return 30
if t in ("полтора часа", "полтора",):
return 5400
if t in ("полторы минуты",):
return 90
m = re.search(r"(\d+)", t)
n = int(m.group(1)) if m else 1
if "сек" in t:
return n
if "час" in t:
return n * 3600
if "мин" in t:
return n * 60
return n * 60 # по умолчанию минуты
def to_tool_call(intent, slots):
"""(intent, slots) → {tool, args} или None для none/неизвестного."""
if intent not in INTENT_TO_TOOL:
return None
tool, args = INTENT_TO_TOOL[intent]
args = dict(args)
if intent == "weather_get" and "city" in slots:
# снять предлог: «в Питере» → «Питере» (город приложение нормализует само)
args["city"] = re.sub(r"\s+", "", slots["city"], flags=re.I)
elif intent == "calendar_create":
if "title" in slots:
args["title"] = slots["title"]
if "date" in slots:
args["date"] = slots["date"] # «25 июля»/«завтра» → YYYY-MM-DD на стороне приложения
if "time" in slots:
args["start_time"] = slots["time"]
args["owner"] = OWNER_MAP.get(slots.get("owner", "").lower().strip(), "daniil")
elif intent in ("timer_set",):
if "duration" in slots:
args["seconds"] = parse_duration_seconds(slots["duration"])
args.setdefault("label", "таймер")
elif intent in ("timer_add", "timer_subtract"):
sign = args.pop("sign", 1)
secs = parse_duration_seconds(slots.get("duration", "1 минута"))
args["delta_seconds"] = sign * secs
args.setdefault("label", "таймер")
elif intent == "purifier_mode":
raw = slots.get("mode", "").lower().strip()
# точное совпадение, затем подстрока (страховка от «ночной режим» и т.п.)
mode = MODE_MAP.get(raw)
if mode is None:
mode = next((v for k, v in MODE_MAP.items() if k in raw), "Auto")
args["mode"] = mode
elif intent in ("light_on", "light_off"):
if "device" in slots:
args["device"] = slots["device"] # приложение сопоставит с entity HA
elif intent == "music_play":
if "query" in slots:
args["query"] = slots["query"]
elif intent == "music_volume":
m = re.search(r"(\d+)", slots.get("level", ""))
if m:
args["volume"] = int(m.group(1))
return {"tool": tool, "args": args}
if __name__ == "__main__":
# быстрая проверка карты без модели
demo = [
("timer_set", {"duration": "15 минут"}),
("timer_add", {"duration": "5 минут"}),
("calendar_create", {"title": "встречу", "date": "25 июля",
"time": "13 часов", "owner": "Дани"}),
("purifier_mode", {"mode": "ночной"}),
("music_volume", {"level": "50 процентов"}),
("weather_get", {"city": "в Питере"}),
]
for intent, slots in demo:
print(f"{intent:16s} {slots} -> {to_tool_call(intent, slots)}")