/** * История диалога per-agent per-day. Файлы в /data/voice-history/{agent}-{date}.json. * /data — это volume контейнера (на хосте /opt/digital-home/smart-home-tablet-data/). */ import { promises as fs } from 'node:fs' import path from 'node:path' const DATA_DIR = process.env.VOICE_HISTORY_DIR || '/data/voice-history' const MAX_HISTORY = parseInt(process.env.VOICE_MAX_HISTORY || '40', 10) export type HistoryMessage = { role: 'user' | 'assistant' content: any } function todayIso(): string { return new Date().toISOString().slice(0, 10) } function historyPath(agent: string): string { return path.join(DATA_DIR, `${agent}-${todayIso()}.json`) } export async function loadHistory(agent: string): Promise { try { const raw = await fs.readFile(historyPath(agent), 'utf-8') const parsed = JSON.parse(raw) return Array.isArray(parsed) ? parsed : [] } catch (e: any) { if (e?.code === 'ENOENT') return [] console.warn('[voice/history] read failed:', e?.message || e) return [] } } export async function saveHistory(agent: string, history: HistoryMessage[]): Promise { try { await fs.mkdir(DATA_DIR, { recursive: true }) const trimmed = history.slice(-MAX_HISTORY) await fs.writeFile(historyPath(agent), JSON.stringify(trimmed, null, 2), 'utf-8') } catch (e: any) { console.warn('[voice/history] write failed:', e?.message || e) } } export async function resetHistory(agent: string): Promise { try { await fs.unlink(historyPath(agent)) } catch (e: any) { if (e?.code !== 'ENOENT') console.warn('[voice/history] reset failed:', e?.message || e) } } /** * Убирает cache_control из блоков (для записи в историю — следующий turn пересчитает границу). */ export function stripCacheControl(content: any): any { if (Array.isArray(content)) { return content.map((b) => { if (b && typeof b === 'object' && 'cache_control' in b) { const { cache_control: _ignore, ...rest } = b return rest } return b }) } return content } /** * Граница prompt-кеша: всё, кроме последних N сообщений, помечаем cache_control * на последнем блоке последнего «старого» сообщения. Даёт cache hit на каждом turn. */ export function buildMessagesWithCache(history: HistoryMessage[], cacheTailUncached = 2): HistoryMessage[] { if (history.length <= cacheTailUncached) { return history.map((m) => ({ role: m.role, content: m.content })) } const cacheBoundary = history.length - cacheTailUncached return history.map((msg, i) => { if (i === cacheBoundary - 1) { return { role: msg.role, content: wrapLastBlockWithCache(msg.content) } } return { role: msg.role, content: msg.content } }) } function wrapLastBlockWithCache(content: any): any { if (typeof content === 'string') { return [{ type: 'text', text: content, cache_control: { type: 'ephemeral' } }] } if (Array.isArray(content) && content.length) { const out = [...content] const last = out[out.length - 1] if (last && typeof last === 'object') { out[out.length - 1] = { ...last, cache_control: { type: 'ephemeral' } } } return out } return content }