Architektur-Umbau: FAQ-Antworten werden NICHT mehr direkt gestreamt. Stattdessen werden die Top-3 relevanten FAQ-Einträge als Kontext ans LLM übergeben. Das LLM interpretiert die Frage, kombiniert mehrere FAQs bei komplexen Fragen und antwortet natürlich. Vorher: Frage → Keyword-Match → FAQ direkt streamen (LLM umgangen) Nachher: Frage → Top-3 FAQ-Matches → LLM-Prompt als Kontext → LLM antwortet Neue Funktionen: - matchFAQMultiple(): Top-N Matches statt nur bester - buildFAQContext(): Baut Kontext-String für LLM-Injection - faqContext statt faqAnswer im Request-Body - System-Prompt Anweisung: "Kombiniere bei Bedarf, natürlicher Fließtext" Behebt: Komplexe Fragen mit 2+ Themen werden jetzt korrekt beantwortet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
import { Language } from '../types'
|
|
import { FAQEntry } from './types'
|
|
import { PRESENTER_FAQ } from './presenter-faq'
|
|
|
|
/**
|
|
* Match a user query against pre-cached FAQ entries.
|
|
* Returns the best match if score exceeds threshold, or null for LLM fallback.
|
|
*/
|
|
export function matchFAQ(query: string, lang: Language): FAQEntry | null {
|
|
const matches = matchFAQMultiple(query, lang, 1)
|
|
return matches.length > 0 ? matches[0] : null
|
|
}
|
|
|
|
/**
|
|
* Match a user query and return the top N relevant FAQ entries as context.
|
|
* Used to feed multiple relevant FAQs into the LLM prompt.
|
|
*/
|
|
export function matchFAQMultiple(query: string, lang: Language, maxResults: number = 3): FAQEntry[] {
|
|
const normalized = query.toLowerCase().trim()
|
|
const queryWords = normalized.split(/\s+/)
|
|
|
|
const scored: { entry: FAQEntry; score: number }[] = []
|
|
|
|
for (const entry of PRESENTER_FAQ) {
|
|
let score = 0
|
|
|
|
// Check keyword matches
|
|
for (const keyword of entry.keywords) {
|
|
const kwLower = keyword.toLowerCase()
|
|
if (kwLower.includes(' ')) {
|
|
if (normalized.includes(kwLower)) {
|
|
score += 3 * entry.priority / 10
|
|
}
|
|
} else {
|
|
if (queryWords.some(w => w === kwLower || w.startsWith(kwLower) || kwLower.startsWith(w))) {
|
|
score += 1
|
|
}
|
|
if (normalized.includes(kwLower)) {
|
|
score += 0.5
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check question text overlap
|
|
const questionText = lang === 'de' ? entry.question_de : entry.question_en
|
|
const questionWords = questionText.toLowerCase().split(/\s+/)
|
|
const overlapCount = queryWords.filter(w =>
|
|
w.length > 2 && questionWords.some(qw => qw.includes(w) || w.includes(qw))
|
|
).length
|
|
if (overlapCount >= 2) {
|
|
score += overlapCount * 0.5
|
|
}
|
|
|
|
score *= (entry.priority / 10)
|
|
|
|
if (score >= 1.0) {
|
|
scored.push({ entry, score })
|
|
}
|
|
}
|
|
|
|
// Sort by score descending, return top N
|
|
scored.sort((a, b) => b.score - a.score)
|
|
return scored.slice(0, maxResults).map(s => s.entry)
|
|
}
|
|
|
|
/**
|
|
* Get FAQ answer text in the requested language
|
|
*/
|
|
export function getFAQAnswer(entry: FAQEntry, lang: Language): string {
|
|
return lang === 'de' ? entry.answer_de : entry.answer_en
|
|
}
|
|
|
|
/**
|
|
* Build a context string from multiple FAQ matches for LLM injection
|
|
*/
|
|
export function buildFAQContext(entries: FAQEntry[], lang: Language): string {
|
|
if (entries.length === 0) return ''
|
|
|
|
const parts = entries.map((entry, idx) => {
|
|
const q = lang === 'de' ? entry.question_de : entry.question_en
|
|
const a = lang === 'de' ? entry.answer_de : entry.answer_en
|
|
return `### Relevante Information ${idx + 1}: ${q}\n${a}`
|
|
})
|
|
|
|
return `\n\n## Vorrecherchierte Antworten (nutze diese als Basis, kombiniere bei Bedarf)\n${parts.join('\n\n')}\n\nWICHTIG: Formuliere die Antwort in deinen eigenen Worten als natürlichen Fließtext. Kombiniere die Informationen wenn die Frage mehrere Themen berührt. Antworte nicht mit Bulletlisten.`
|
|
}
|