From db942fd7349cf906ce6db758ebf766d2658512c4 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Tue, 14 Apr 2026 13:18:53 -0400 Subject: [PATCH] Add comprehensive chatbot improvements: better LLM prompts, caching, confidence labels, smarter follow-ups --- server.js | 59 +++++++++++++++++++++++++++++++++++++++++++++-------- src/App.tsx | 29 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index b38955a..a18bf8d 100644 --- a/server.js +++ b/server.js @@ -89,6 +89,34 @@ let lastVisitorStatsWrite = { ok: true, at: null, error: null } let lastHitStatsWrite = { ok: true, at: null, error: null } let lastBackupStatus = { ok: true, at: null, error: null, file: null } const adminSessions = new Map() +const llmResponseCache = new Map() +const MAX_CACHE_SIZE = 500 +const CACHE_TTL_MS = 60 * 60 * 1000 + +function getCacheKey(question, draftAnswer) { + return sha256(`${question}||${draftAnswer}`).slice(0, 16) +} + +function getCachedResponse(cacheKey) { + const cached = llmResponseCache.get(cacheKey) + if (cached && Date.now() - cached.at < CACHE_TTL_MS) { + return cached.text + } + if (cached) { + llmResponseCache.delete(cacheKey) + } + return null +} + +function setCachedResponse(cacheKey, text) { + if (llmResponseCache.size >= MAX_CACHE_SIZE) { + const firstKey = llmResponseCache.keys().next().value + if (firstKey) llmResponseCache.delete(firstKey) + } + llmResponseCache.set(cacheKey, { text, at: Date.now() }) +} + +function sha256(value) function sha256(value) { return createHash('sha256').update(value).digest('hex') @@ -829,13 +857,20 @@ function buildGroundedRewritePrompt({ question, draftAnswer, sources, contextChu : 'No context excerpts were provided.' return [ - 'You are rewriting a Bible-study chatbot answer.', - 'Hard rules:', - '1) Use ONLY facts in DRAFT ANSWER and CONTEXT EXCERPTS.', - '2) If DRAFT ANSWER is uncertain or says content is missing, keep that uncertainty and do not fill gaps.', - '3) Do not add verses, names, history, or claims not present in the provided text.', - '4) Keep a warm pastoral tone but concise.', - '5) Return plain text only. No markdown headers, no bullets unless needed for clarity.', + 'You are a Bible study assistant helping rewrite responses to be clearer and more pastoral.', + '', + 'HARD RULES:', + '1) Use ONLY facts from DRAFT ANSWER and CONTEXT EXCERPTS. Never add new information.', + '2) If the draft is uncertain or incomplete, preserve that. Do not fill gaps or speculate.', + '3) Do not invent verses, names, historical details, or theological claims.', + '4) Maintain a warm, encouraging pastoral tone—like Nate teaching directly.', + '5) Keep sentences clear and direct. Avoid jargon unless biblical.', + '6) Return plain text only. Use paragraph breaks but no markdown formatting.', + '', + 'GOALS:', + '- Help the reader understand Scripture better', + '- Stay faithful to Nate\'s teaching and tone', + '- Be encouraging but honest about limitations', '', `QUESTION:\n${question}`, '', @@ -850,6 +885,12 @@ function buildGroundedRewritePrompt({ question, draftAnswer, sources, contextChu async function rewriteWithChatbotLlm({ question, draftAnswer, sources, contextChunks }) { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), CHATBOT_LLM_TIMEOUT_MS) + const cacheKey = getCacheKey(question, draftAnswer) + const cached = getCachedResponse(cacheKey) + if (cached) { + return cached + } + try { const prompt = buildGroundedRewritePrompt({ @@ -897,7 +938,9 @@ async function rewriteWithChatbotLlm({ question, draftAnswer, sources, contextCh throw new Error('LLM returned an empty response') } - return rewritten.slice(0, 3500) + const result = rewritten.slice(0, 3500) + setCachedResponse(cacheKey, result) + return result } finally { clearTimeout(timeout) } diff --git a/src/App.tsx b/src/App.tsx index c0f2ebe..2d0f026 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1062,6 +1062,22 @@ function buildSmartFollowUpPrompts(chunks: SearchChunk[], analysis: IntentAnalys .slice(0, 3) .map(prompt => ({ label: prompt, prompt })) + // Add context-specific follow-ups based on what was discussed + const subjectContext = analysis.subject || extractIdentitySubject(analysis.rawQuery.toLowerCase()) + if (subjectContext && !analysis.rawQuery.toLowerCase().includes('apply')) { + promptSuggestions.unshift({ + label: `How does this apply to my life?`, + prompt: `Based on what you just said about ${subjectContext}, how should I apply this today?` + }) + } + + if (primaryVerse && analysis.type === 'identity') { + promptSuggestions.unshift({ + label: `What's the historical context?`, + prompt: `What was the historical and cultural context of ${formatVerseRef(primaryVerse)}?` + }) + } + const detailedNotesReply = buildDetailedNotesReplyFromChunks(chunks) const canOfferDetailedNotes = hasMeaningfulExtraDetail(shortAnswer, detailedNotesReply) @@ -1152,6 +1168,18 @@ function synthesizeSmartReply(scored: ScoredChunk[], analysis: IntentAnalysis, c } } +function addConfidenceLabel(response: ChatResponse, confidence: 'high' | 'medium' | 'low'): ChatResponse { + const label = confidence === 'high' + ? '✓ High confidence' + : confidence === 'medium' + ? '~ Medium confidence' + : '? Low confidence' + return { + ...response, + text: `${response.text}\n\n[${label}]` + } + } + function buildNextChatContext(query: string, analysis: IntentAnalysis, scored: ScoredChunk[], previousContext: ChatContextState): ChatContextState { const topChunks = getTopDistinctChunks(scored, 3) if (topChunks.length === 0) return EMPTY_CHAT_CONTEXT @@ -1449,6 +1477,7 @@ function ChatBot({ mode = 'embedded' }: { mode?: 'embedded' | 'standalone' }) { if (confidence !== 'low') { shouldTryLlmRewrite = true chatContextRef.current = buildNextChatContext(contextualQuery, analysis, scored, activeContext) + botReply = addConfidenceLabel(botReply, confidence) } } else { botReply = buildSmartFallbackReply(analyzeIntent(contextualQuery, activeContext), activeContext)