/** * Быстрый путь через локальный агент (tablet-agent) — sidecar-служба * intent+slots на мини-ПК. Распознаёт команды офлайн за ~2 мс и исполняет * их напрямую, минуя облачный LLM. * * Включается переменной окружения AGENT_URL (напр. http://agent:8091). * Если она не задана — tryLocalAgent сразу возвращает null и планшет работает * ровно как раньше (облачный LLM). Любая ошибка службы → тоже null → fallback. * * Разделение ответственности: * • Команды-ДЕЙСТВИЯ (таймер/свет/музыка/очиститель/событие) — исполняет * локальный агент и отвечает коротким подтверждением. Быстро, офлайн. * • Запросы ИНФОРМАЦИИ (погода/календарь/что играет/состояние дома) и всё * неуверенное/none — проваливаются в облачный LLM: он лучше формулирует * ответ по данным. */ import { executeTool } from './tools/_registry' import type { AgentId } from './tools/_types' const THRESHOLD = parseFloat(process.env.AGENT_THRESHOLD || '0.7') // Интенты-действия, которые локальный агент исполняет сам (короткое подтверждение). // Информационные интенты (weather_get, transport_*, calendar_today/week, // notes_get, home_state, now_playing) сюда НЕ входят — их формулирует LLM. const ACTION_INTENTS = new Set([ 'timer_set', 'timer_add', 'timer_subtract', 'timer_cancel', 'purifier_on', 'purifier_off', 'purifier_mode', 'light_on', 'light_off', 'music_play', 'music_pause', 'music_resume', 'music_next', 'music_prev', 'music_volume', 'calendar_create', ]) interface AgentResponse { intent: string slots: Record score: number tool: { tool: string; args: Record } | null } async function queryAgent(text: string): Promise { const base = process.env.AGENT_URL if (!base) return null const r = await fetch(`${base.replace(/\/$/, '')}/predict`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), signal: AbortSignal.timeout(1500), }) if (!r.ok) return null return r.json() } // Короткие естественные подтверждения по интенту. Используют слоты, где уместно. function confirmText(intent: string, slots: Record): string { const dur = slots.duration ? ` на ${slots.duration}` : '' switch (intent) { case 'timer_set': return `Поставила таймер${dur}.` case 'timer_add': return `Добавила${slots.duration ? ' ' + slots.duration : ' время'} к таймеру.` case 'timer_subtract': return `Убавила${slots.duration ? ' ' + slots.duration : ''} у таймера.` case 'timer_cancel': return `Таймер отменила.` case 'purifier_on': return `Включаю очиститель.` case 'purifier_off': return `Выключаю очиститель.` case 'purifier_mode': return `Переключаю режим очистителя${slots.mode ? ' на ' + slots.mode : ''}.` case 'light_on': return `Включаю ${slots.device || 'свет'}.` case 'light_off': return `Выключаю ${slots.device || 'свет'}.` case 'music_play': return `Включаю${slots.query ? ' ' + slots.query : ' музыку'}.` case 'music_pause': return `Поставила на паузу.` case 'music_resume': return `Продолжаю.` case 'music_next': return `Следующий трек.` case 'music_prev': return `Предыдущий трек.` case 'music_volume': return `Готово, громкость изменила.` case 'calendar_create': return `Добавила событие${slots.title ? ' «' + slots.title + '»' : ''}${slots.date ? ' на ' + slots.date : ''}.` default: return `Готово.` } } /** * Пытается обработать запрос локальным агентом. * Возвращает { text } если команда-действие исполнена, иначе null (→ LLM). */ export async function tryLocalAgent( text: string, agent: AgentId, ): Promise<{ text: string; intent: string } | null> { const res = await queryAgent(text) if (!res) return null if (res.score < THRESHOLD) return null if (!ACTION_INTENTS.has(res.intent)) return null // инфо-запросы → LLM if (!res.tool) return null const result = await executeTool(res.tool.tool, res.tool.args, agent) if (result && (result as any).error) return null // ошибка tool → пусть LLM разберётся/озвучит return { text: confirmText(res.intent, res.slots || {}), intent: res.intent } }