Files
smart-home-tablet/lib/agent-local.ts
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

95 lines
5.1 KiB
TypeScript
Raw 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.
/**
* Быстрый путь через локальный агент (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<string, string>
score: number
tool: { tool: string; args: Record<string, any> } | null
}
async function queryAgent(text: string): Promise<AgentResponse | null> {
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, string>): 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 }
}