Add comprehensive chatbot improvements: better LLM prompts, caching, confidence labels, smarter follow-ups

This commit is contained in:
nmemmert
2026-04-14 13:18:53 -04:00
parent 67b7d1024b
commit db942fd734
2 changed files with 80 additions and 8 deletions
+51 -8
View File
@@ -89,6 +89,34 @@ let lastVisitorStatsWrite = { ok: true, at: null, error: null }
let lastHitStatsWrite = { ok: true, at: null, error: null } let lastHitStatsWrite = { ok: true, at: null, error: null }
let lastBackupStatus = { ok: true, at: null, error: null, file: null } let lastBackupStatus = { ok: true, at: null, error: null, file: null }
const adminSessions = new Map() 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) { function sha256(value) {
return createHash('sha256').update(value).digest('hex') return createHash('sha256').update(value).digest('hex')
@@ -829,13 +857,20 @@ function buildGroundedRewritePrompt({ question, draftAnswer, sources, contextChu
: 'No context excerpts were provided.' : 'No context excerpts were provided.'
return [ return [
'You are rewriting a Bible-study chatbot answer.', 'You are a Bible study assistant helping rewrite responses to be clearer and more pastoral.',
'Hard rules:', '',
'1) Use ONLY facts in DRAFT ANSWER and CONTEXT EXCERPTS.', 'HARD RULES:',
'2) If DRAFT ANSWER is uncertain or says content is missing, keep that uncertainty and do not fill gaps.', '1) Use ONLY facts from DRAFT ANSWER and CONTEXT EXCERPTS. Never add new information.',
'3) Do not add verses, names, history, or claims not present in the provided text.', '2) If the draft is uncertain or incomplete, preserve that. Do not fill gaps or speculate.',
'4) Keep a warm pastoral tone but concise.', '3) Do not invent verses, names, historical details, or theological claims.',
'5) Return plain text only. No markdown headers, no bullets unless needed for clarity.', '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}`, `QUESTION:\n${question}`,
'', '',
@@ -850,6 +885,12 @@ function buildGroundedRewritePrompt({ question, draftAnswer, sources, contextChu
async function rewriteWithChatbotLlm({ question, draftAnswer, sources, contextChunks }) { async function rewriteWithChatbotLlm({ question, draftAnswer, sources, contextChunks }) {
const controller = new AbortController() const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), CHATBOT_LLM_TIMEOUT_MS) const timeout = setTimeout(() => controller.abort(), CHATBOT_LLM_TIMEOUT_MS)
const cacheKey = getCacheKey(question, draftAnswer)
const cached = getCachedResponse(cacheKey)
if (cached) {
return cached
}
try { try {
const prompt = buildGroundedRewritePrompt({ const prompt = buildGroundedRewritePrompt({
@@ -897,7 +938,9 @@ async function rewriteWithChatbotLlm({ question, draftAnswer, sources, contextCh
throw new Error('LLM returned an empty response') 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 { } finally {
clearTimeout(timeout) clearTimeout(timeout)
} }
+29
View File
@@ -1062,6 +1062,22 @@ function buildSmartFollowUpPrompts(chunks: SearchChunk[], analysis: IntentAnalys
.slice(0, 3) .slice(0, 3)
.map(prompt => ({ label: prompt, prompt })) .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 detailedNotesReply = buildDetailedNotesReplyFromChunks(chunks)
const canOfferDetailedNotes = hasMeaningfulExtraDetail(shortAnswer, detailedNotesReply) 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 { function buildNextChatContext(query: string, analysis: IntentAnalysis, scored: ScoredChunk[], previousContext: ChatContextState): ChatContextState {
const topChunks = getTopDistinctChunks(scored, 3) const topChunks = getTopDistinctChunks(scored, 3)
if (topChunks.length === 0) return EMPTY_CHAT_CONTEXT if (topChunks.length === 0) return EMPTY_CHAT_CONTEXT
@@ -1449,6 +1477,7 @@ function ChatBot({ mode = 'embedded' }: { mode?: 'embedded' | 'standalone' }) {
if (confidence !== 'low') { if (confidence !== 'low') {
shouldTryLlmRewrite = true shouldTryLlmRewrite = true
chatContextRef.current = buildNextChatContext(contextualQuery, analysis, scored, activeContext) chatContextRef.current = buildNextChatContext(contextualQuery, analysis, scored, activeContext)
botReply = addConfidenceLabel(botReply, confidence)
} }
} else { } else {
botReply = buildSmartFallbackReply(analyzeIntent(contextualQuery, activeContext), activeContext) botReply = buildSmartFallbackReply(analyzeIntent(contextualQuery, activeContext), activeContext)