-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgrounding-validator.ts
More file actions
130 lines (115 loc) · 4.45 KB
/
Copy pathgrounding-validator.ts
File metadata and controls
130 lines (115 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { VisitContext } from "./schemas/visit-context.ts";
export const DEFAULT_ALLOWED_CITATION_IDS = [
"CIT-001",
"CIT-002",
"CIT-003",
"CIT-004",
"CIT-005",
"CIT-006",
"CIT-007",
"CIT-008",
"CIT-009",
"CIT-010",
] as const;
export const CITATION_SNIPPETS: Record<string, string> = {
"CIT-001":
"AHRQ plain-language materials should use common words, short sentences, active voice, direct organization, and focus on what the patient must do.",
"CIT-002":
"AHRQ PEMAT evaluates patient education materials for understandability and actionability; scores of at least 70 percent are used as a patient-ready benchmark.",
"CIT-003":
"Patients forget or misremember a substantial portion of medical information provided during clinical encounters.",
"CIT-004":
"Patients may misunderstand discharge instructions and may not realize that they have not understood a care instruction.",
"CIT-005":
"CDC clear communication guidance emphasizes the main message, clear calls to action, and behavioral recommendations.",
"CIT-006":
"AHRQ discharge process guidance emphasizes medication clarity, follow-up instructions, return precautions, and contact information.",
"CIT-007":
"Joint Commission discharge standards expect care transitions to reflect the patient's assessed needs.",
"CIT-008":
"CMS discharge planning requirements emphasize safe transitions and patient-relevant follow-up planning.",
"CIT-009":
"HL7 FHIR R4 defines the healthcare resource shapes used for read and write workflows.",
"CIT-010":
"SHARP-on-MCP defines stateless healthcare context propagation with X-FHIR-Server-URL, X-FHIR-Access-Token, and X-Patient-ID headers.",
};
export interface GroundingValidationInput {
text: string;
visit_context: VisitContext;
allowed_citation_ids?: string[];
citations_used?: string[];
}
export interface GroundingValidationResult {
ok: boolean;
citations_used: string[];
unapproved_citations: string[];
unsupported_quotes: string[];
unknown_doses: string[];
}
function normalize(text: string): string {
return text
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, " ")
.trim()
.replace(/\s+/g, " ");
}
function wordCount(text: string): number {
return normalize(text).split(/\s+/).filter(Boolean).length;
}
function extractQuotedPhrases(text: string): string[] {
const out: string[] = [];
const re = /["“”]([^"“”]{1,800})["“”]/g;
let match = re.exec(text);
while (match !== null) {
const phrase = match[1]?.trim();
if (phrase && wordCount(phrase) >= 6) out.push(phrase);
match = re.exec(text);
}
return out;
}
function extractCitationIds(text: string): string[] {
return Array.from(new Set(text.match(/\bCIT-\d{3}\b/g) ?? []));
}
function extractDoseStrings(text: string): string[] {
const matches = text.match(
/\b\d+(?:\.\d+)?\s*(?:mg|mcg|g|mEq|units?)\b(?:\s*(?:po|oral|daily|bid|qhs|prn))?/gi,
);
return Array.from(new Set(matches ?? []));
}
function chartCorpus(visitContext: VisitContext): string {
const medicationBits = visitContext.medication_changes
.flatMap((m) => [m.name, m.dose, m.reason ?? "", m.behavior_rule ?? ""])
.join(" ");
return `${JSON.stringify(visitContext)} ${medicationBits}`;
}
function citationCorpus(ids: string[]): string {
return ids.map((id) => CITATION_SNIPPETS[id] ?? "").join(" ");
}
export function validateGrounding(input: GroundingValidationInput): GroundingValidationResult {
const allowed = input.allowed_citation_ids ?? [...DEFAULT_ALLOWED_CITATION_IDS];
const citations_used = Array.from(
new Set([...(input.citations_used ?? []), ...extractCitationIds(input.text)]),
);
const allowedSet = new Set(allowed);
const unapproved_citations = citations_used.filter((id) => !allowedSet.has(id));
const corpus = normalize(`${chartCorpus(input.visit_context)} ${citationCorpus(allowed)}`);
const unsupported_quotes = extractQuotedPhrases(input.text).filter(
(phrase) => !corpus.includes(normalize(phrase)),
);
const allowedDoseCorpus = normalize(chartCorpus(input.visit_context));
const unknown_doses = extractDoseStrings(input.text).filter(
(dose) => !allowedDoseCorpus.includes(normalize(dose)),
);
return {
ok:
unapproved_citations.length === 0 &&
unsupported_quotes.length === 0 &&
unknown_doses.length === 0,
citations_used,
unapproved_citations,
unsupported_quotes,
unknown_doses,
};
}