voice: усиление умного дома — контекст, промпт, новые tools
All checks were successful
Deploy / deploy (push) Successful in 1m52s
All checks were successful
Deploy / deploy (push) Successful in 1m52s
- промпт: перечислены ВСЕ 15+ tools, разрешено уверенно управлять домом - route.ts: живой контекст (время + снимок устройств из HA) в системный промпт для LLM-пути (defensive, с таймаутом) - новые tools: control_climate (кондей/термостат), control_tv, set_scene (night/movie/morning/away), all_off, ha_service (универсальный HA с whitelist) - env: CLIMATE_ENTITY, TV_ENTITY (см. RUNBOOK)
This commit is contained in:
@@ -117,6 +117,45 @@ function emitVoice(event: string, agent: AgentId, text?: string) {
|
||||
voiceBus.emit('voice', { event, agent, text, timestamp: new Date().toISOString() })
|
||||
}
|
||||
|
||||
// Живой контекст для системного промпта: текущее время + снимок устройств из HA.
|
||||
// Defensive: любая ошибка/таймаут → только время, без падения запроса.
|
||||
async function buildLiveContext(): Promise<string> {
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleString('ru-RU', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit',
|
||||
})
|
||||
try {
|
||||
const base = `http://localhost:${process.env.PORT || '3000'}`
|
||||
const r = await fetch(`${base}/api/ha`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'x-voice-internal': process.env.VOICE_API_KEY || '' },
|
||||
signal: AbortSignal.timeout(1200),
|
||||
})
|
||||
if (!r.ok) return `\n\nСейчас: ${timeStr}.`
|
||||
const d = await r.json()
|
||||
const states = d.states || {}
|
||||
const s = d.sensors || {}
|
||||
const lines: string[] = []
|
||||
for (const [id, st] of Object.entries<any>(states)) {
|
||||
const name = st.attributes?.friendly_name || id
|
||||
let extra = ''
|
||||
if (st.attributes?.preset_mode) extra += `, режим ${st.attributes.preset_mode}`
|
||||
if (st.attributes?.current_temperature != null) extra += `, ${st.attributes.current_temperature}°`
|
||||
lines.push(` ${name} (${id}): ${st.state}${extra}`)
|
||||
}
|
||||
const sensorLine = [
|
||||
s.temperature != null ? `дома ${s.temperature}°` : '',
|
||||
s.humidity != null ? `влажность ${s.humidity}%` : '',
|
||||
s.pm25 != null ? `PM2.5 ${s.pm25}` : '',
|
||||
].filter(Boolean).join(', ')
|
||||
if (!lines.length && !sensorLine) return `\n\nСейчас: ${timeStr}.`
|
||||
return `\n\nСнимок дома (${timeStr}):\n${sensorLine ? ' ' + sensorLine + '\n' : ''}${lines.join('\n')}\n` +
|
||||
`Для устройств без специального tool используй ha_service с entity_id выше.`
|
||||
} catch {
|
||||
return `\n\nСейчас: ${timeStr}.`
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const cookie = req.headers.get('cookie') || ''
|
||||
const tokenMatch = cookie.match(/auth_token=([a-f0-9]{32,})/i)
|
||||
@@ -164,6 +203,11 @@ export async function POST(req: Request) {
|
||||
console.log('[voice/chat] local-agent skip:', (e as Error).message)
|
||||
}
|
||||
|
||||
// Живой контекст (время + снимок устройств) — только для LLM-пути (fast-path
|
||||
// выше уже вышел). Даёт модели понимание, что включено и что вообще есть в
|
||||
// доме, чтобы рассуждать про умный дом и не переспрашивать очевидное.
|
||||
const fullSys = sysPrompt + (await buildLiveContext())
|
||||
|
||||
// ======== GROQ ========
|
||||
if (provider === 'groq') {
|
||||
const groqModel = settings.groqModel || 'llama-3.3-70b-versatile'
|
||||
@@ -175,7 +219,7 @@ export async function POST(req: Request) {
|
||||
return m
|
||||
})
|
||||
const groqMessages: any[] = [
|
||||
{ role: 'system', content: sysPrompt },
|
||||
{ role: 'system', content: fullSys },
|
||||
...normalizedHistory,
|
||||
{ role: 'user', content: userText },
|
||||
]
|
||||
@@ -218,7 +262,7 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
const fb = await groqClient().chat.completions.create({
|
||||
model: groqModel, max_tokens: MAX_TOKENS,
|
||||
messages: [{ role: 'system', content: sysPrompt }, { role: 'user', content: userText }],
|
||||
messages: [{ role: 'system', content: fullSys }, { role: 'user', content: userText }],
|
||||
})
|
||||
finalText = fb.choices[0]?.message?.content || 'Не удалось выполнить запрос.'
|
||||
newTurns.push({ role: 'assistant', content: finalText })
|
||||
@@ -237,7 +281,7 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const t0 = Date.now()
|
||||
const resp = await claudeRequest(sysPrompt, messages, anthropicTools)
|
||||
const resp = await claudeRequest(fullSys, messages, anthropicTools)
|
||||
console.log(`[voice/chat] claude ${agent} r${round+1} ${Date.now()-t0}ms stop=${resp.stop_reason} in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`)
|
||||
const content: any[] = resp.content || []
|
||||
const stopReason = resp.stop_reason || 'end_turn'
|
||||
|
||||
Reference in New Issue
Block a user