#!/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)}")