| import { ChatMessage, ChatRequest, SearchSummary } from "../types.js"; |
| import { memoryStore } from "./memory.js"; |
| import { summarizeExtractive, totalTokens } from "./context.js"; |
| import { buildSystemPrompt, shouldSearch, trimHistory } from "./router.js"; |
| import { searchWeb } from "../search/search.js"; |
| import { streamChatCompletion } from "../utils/stream.js"; |
|
|
| const MAX_CONTEXT_TOKENS = 2600; |
|
|
| export type EngineEvents = { |
| onStatus: (message: string) => void; |
| onToken: (token: string) => void; |
| onDone: () => void; |
| onError: (message: string) => void; |
| }; |
|
|
| function buildSearchContext(summary: SearchSummary) { |
| const lines: string[] = []; |
| summary.results.forEach((r, idx) => { |
| lines.push(`Source ${idx + 1}: ${r.title} (${r.url})`); |
| lines.push(r.snippet); |
| }); |
| summary.topContent.forEach((c, idx) => { |
| lines.push(`Content ${idx + 1}: ${c.title}`); |
| lines.push(c.content.slice(0, 1200)); |
| }); |
| return lines.join("\n"); |
| } |
|
|
| export async function handleChat(request: ChatRequest, events: EngineEvents) { |
| const session = memoryStore.get(request.sessionId); |
| const userMessage: ChatMessage = { |
| id: `msg_${Date.now()}`, |
| role: "user", |
| content: request.message, |
| createdAt: Date.now() |
| }; |
|
|
| session.messages.push(userMessage); |
|
|
| const useSearch = shouldSearch(request.message, request.mode ?? "auto"); |
| let searchSummary: SearchSummary | null = null; |
|
|
| if (useSearch) { |
| events.onStatus("Searching web..."); |
| try { |
| searchSummary = await searchWeb(request.message); |
| events.onStatus("Summarizing sources..."); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : "Search failed"; |
| events.onStatus("Search failed, continuing without web context."); |
| events.onError(msg); |
| } |
| } |
|
|
| if (totalTokens(session.messages) > MAX_CONTEXT_TOKENS) { |
| const trimmed = trimHistory(session.messages, 12); |
| session.summary = summarizeExtractive(trimmed, 6); |
| session.messages = trimmed; |
| } |
|
|
| const searchContext = searchSummary ? buildSearchContext(searchSummary) : null; |
| const systemPrompt = buildSystemPrompt(session.summary, searchContext); |
|
|
| const payloadMessages = [ |
| { role: "system", content: systemPrompt }, |
| ...session.messages.map((m) => ({ role: m.role, content: m.content })) |
| ]; |
|
|
| const assistantMessage: ChatMessage = { |
| id: `msg_${Date.now()}_assistant`, |
| role: "assistant", |
| content: "", |
| createdAt: Date.now() |
| }; |
|
|
| try { |
| await streamChatCompletion(payloadMessages, (token) => { |
| assistantMessage.content += token; |
| events.onToken(token); |
| }); |
| session.messages.push(assistantMessage); |
| memoryStore.set(request.sessionId, session); |
| events.onDone(); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : "LLM call failed"; |
| events.onError(msg); |
| } |
| }
|
|
|