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