apps/web/src/lib/specificKnowledgeFact.ts
1export type SpecificKnowledgeFact = { entity: string; attributes: string[] }
2
3const normalize = (text: string) =>
4 text
5 .normalize('NFD')
6 .replace(/[\u0300-\u036f]/g, '')
7 .toLowerCase()
8 .replace(/\b(?:the|in)\b/g, '')
9 .replace(/[^a-z0-9]+/g, ' ')
10 .trim()
11
12/** Narrow named room/document facts need evidence for both the entity and the requested field. */
13export function specificKnowledgeFact(
14 message: string,
15): SpecificKnowledgeFact | undefined {
16 const match = message
17 .trim()
18 .match(
19 /^(?:what(?:['’]s| is) (?:the )?|which (?:is the )?)(.+?) (?:for|in|of|at) (.+?)[?.!]*$/i,
20 )
21 if (
22 !match ||
23 !/\b(?:room|sign|phrase|location|name)\b/i.test(match[1]) ||
24 !/\b(?:room|lounge|hall|workshop|leaflet|document)\b/i.test(
25 `${match[1]} ${match[2]}`,
26 )
27 )
28 return
29 const entity = match[2].replace(/^the\s+/i, '').trim()
30 if (!/[A-Z][a-z]+|[A-Z]{2,}/.test(entity)) return
31 const attributes = normalize(match[1])
32 .split(' ')
33 .filter(
34 (word) =>
35 !['published', 'official', 'current', 'name', 'location'].includes(word),
36 )
37 if (!attributes.length) return
38 return { entity, attributes }
39}
40
41/** Related rooms or documents are not replacement answers after withdrawal. */
42export function supportsSpecificKnowledgeFact(
43 fact: SpecificKnowledgeFact,
44 text: string,
45): boolean {
46 const blocks = /(?:^|\n)Q:/.test(text)
47 ? Array.from(
48 text.matchAll(
49 /(?:^|\n)((?:Q:[^\n]*\n)+)A:\s*([^\n]+(?:\n(?!Q:|\[)[^\n]+)*)/g,
50 ),
51 (match) => `${match[1]}${match[2]}`,
52 )
53 : text.split(/\n(?=\[)/)
54 return blocks.some((block) => {
55 // FAQ question and answer must remain together; a different alias cannot lend its field.
56 const normalized = normalize(block)
57 const words = new Set(normalized.split(' '))
58 return (
59 normalized.includes(normalize(fact.entity)) &&
60 fact.attributes.every((word) => words.has(word))
61 )
62 })
63}
64