/* * conversationTools.ts * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Conversation-level tools: summarize (via the conversation's provider using * its cheapest model) and context auto-trim (drop oldest turns, keep the * system prompt, when near the window limit). */ import { cheapestModel, estimateTokens, findModel } from './catalogHelpers' import { clientFor } from '../providers/registry' import { uid, useStore } from '../state/store' import { getKey } from '../storage/keys' import type { Conversation, Message } from '../types' /** Appends an assistant summary turn produced by the provider's cheapest model. */ export async function summarizeConversation(conversationID: string): Promise { const state = useStore.getState() const conversation = state.conversations.find((c) => c.id === conversationID) if (!conversation || conversation.messages.length === 0) return const model = cheapestModel(conversation.provider) ?? findModel(conversation.provider, conversation.modelID) const apiKey = model ? getKey(model.provider) : undefined if (!model || !apiKey) { state.toast('No key available to summarize with', 'error') return } state.toast('Summarizing conversation…', 'info') const transcript = conversation.messages .filter((m) => !m.errorText) .map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.text.slice(0, 1500)}`) .join('\n\n') try { const result = await clientFor(model).complete( { model, messages: [ { id: uid(), role: 'user', text: `Summarize this conversation in a concise bullet list (key points, decisions, open questions):\n\n${transcript.slice(0, 24_000)}`, createdAt: Date.now(), }, ], parameters: { maxTokens: 600 }, stream: false, }, apiKey ) const summary: Message = { id: uid(), role: 'assistant', text: `**Conversation summary**\n\n${result.text}`, modelID: model.id, provider: model.provider, createdAt: Date.now(), } state.updateConversation(conversationID, { messages: [...conversation.messages, summary], }) } catch (err) { state.toast(err instanceof Error ? err.message : 'Summarize failed', 'error') } } /** * Auto-trim: when the estimated context usage exceeds ~90% of the model's * window, drops the oldest non-system turns (keeping the most recent ones) * and returns the trimmed message list for the request. */ export function autoTrimForContext(conversation: Conversation): { messages: Message[] trimmed: number } { const model = findModel(conversation.provider, conversation.modelID) if (!model) return { messages: conversation.messages, trimmed: 0 } const budget = model.contextWindow * 0.9 const systemTokens = estimateTokens(conversation.systemPrompt ?? '') const usable = conversation.messages.filter((m) => !m.errorText) let total = systemTokens + usable.reduce((sum, m) => sum + estimateTokens(m.text), 0) if (total <= budget) return { messages: conversation.messages, trimmed: 0 } const kept = [...usable] let trimmed = 0 while (total > budget && kept.length > 2) { const dropped = kept.shift() if (!dropped) break total -= estimateTokens(dropped.text) trimmed++ } return { messages: kept, trimmed } }