Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/lib/clinical-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,10 @@ const genericMedicationDoseQueryTokens = new Set([
"usual",
"im",
"po",
"patient",
"recommend",
"recommendation",
"recommended",
"sc",
"sl",
]);
Expand All @@ -1140,6 +1144,13 @@ export function medicationDoseQuerySubjectTokens(query: string) {
return normalizedClinicalSearchTokens(query).filter((token) => !genericMedicationDoseQueryTokens.has(token));
}

/** Whether the query explicitly asks for dose, route, or frequency evidence. */
export function isMedicationDoseEvidenceQuery(query: string) {
return /\b(?:dose|doses|dosage|dosages|dosing|route|oral|intramuscular|subcutaneous|subcut|sublingual|im|po|sc|sl|frequency|mg|mcg|microgram|maximum|minimum|prn)\b/i.test(
query,
);
}
Comment thread
BigSimmo marked this conversation as resolved.

/** Require dose evidence to carry the medication question's clinical subject. */
export function medicationDoseQueryContext(query: string, result: SearchResult) {
const subjectTokens = medicationDoseQuerySubjectTokens(query);
Expand Down
20 changes: 16 additions & 4 deletions src/lib/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import {
expandClinicalQuery,
hasDoseEvidenceSupport,
hasStructuredThresholdEvidence,
isMedicationDoseEvidenceQuery,
medicationDoseQueryContext,
normalizedClinicalSearchTokens,
rankClinicalResults,
Expand Down Expand Up @@ -2608,6 +2609,10 @@ export function decideTextFastPath(
if (queryClass === "medication_dose_risk" && !results.slice(0, 5).some((result) => hasDoseEvidenceSupport(result))) {
return { returnFastPath: false, reason: "missing_dose_evidence" };
}
if (queryClass === "medication_dose_risk" && isMedicationDoseEvidenceQuery(query)) {
const doseCoverage = evaluateEvidenceCoverageGate(query, results, queryClass);
if (!doseCoverage.accepted) return { returnFastPath: false, reason: doseCoverage.reason };
}

if (queryClass === "table_threshold") {
if (strongestScore >= 0.62 || topTextRank >= 0.045) {
Expand Down Expand Up @@ -2921,10 +2926,14 @@ export function evaluateEvidenceCoverageGate(
}

if (queryClass === "medication_dose_risk") {
const asksDoseAmount = /\b(?:dose|doses|dosage|dosages|dosing|mg|mcg|microgram|maximum|minimum)\b/i.test(query);
const asksRoute =
/\b(?:route|oral|intramuscular|subcutaneous|subcut|sublingual|\bim\b|\bpo\b|\bsc\b|\bsl\b)\b/i.test(query);
const agitationOk = !/\bagitation|arousal\b/i.test(query) || /\bagitation|arousal\b/i.test(evidenceText);
const hasContextualDose = top.some(
const hasContextualDoseEvidence = top.some(
(result) => hasDoseEvidenceSupport(result) && medicationDoseQueryContext(query, result).matched,
);
const hasContextualDoseAmount = top.some(
(result) =>
hasDoseEvidenceSupport(result) &&
hasDoseAmountEvidenceForGate(result) &&
Expand All @@ -2933,18 +2942,21 @@ export function evaluateEvidenceCoverageGate(
const hasContextualRoute = top.some(
(result) =>
hasDoseEvidenceSupport(result) &&
hasDoseAmountEvidenceForGate(result) &&
hasRouteEvidenceForGate(result) &&
medicationDoseQueryContext(query, result).matched,
);
const accepted = hasContextualDose && (!asksRoute || hasContextualRoute) && agitationOk;
const accepted =
hasContextualDoseEvidence &&
(!asksDoseAmount || hasContextualDoseAmount) &&
(!asksRoute || hasContextualRoute) &&
Comment thread
BigSimmo marked this conversation as resolved.
agitationOk;
return {
accepted,
reason: accepted
? "dose_route_amount_evidence_gate"
: !hasDoseAmount
? "missing_dose_amount_evidence"
: !hasContextualDose
: !hasContextualDoseEvidence || (asksDoseAmount && !hasContextualDoseAmount)
? "missing_dose_query_context"
: !hasContextualRoute && asksRoute
? "missing_route_evidence"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
2 changes: 1 addition & 1 deletion tests/rag-answer-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async function answerFromTextSources(
});

vi.doMock("@/lib/openai", () => ({
embedTextWithTelemetry: vi.fn(),
embedTextWithTelemetry: vi.fn(async () => ({ embedding: [0.1, 0.2, 0.3], cacheHit: false })),
generateStructuredTextResult,
}));

Expand Down
24 changes: 24 additions & 0 deletions tests/retrieval-query-variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,30 @@ describe("retrieval query variants", () => {
"medication_dose_risk",
),
).toEqual({ returnFastPath: true, reason: "dose_evidence_text_match" });

expect(
decideTextFastPath(
"How should agitation be managed when oral medication is refused?",
[
result({
content: "For agitation, use IM medication when oral medication is refused, with review and monitoring.",
similarity: 0.67,
}),
],
"medication_dose_risk",
),
).toEqual({ returnFastPath: true, reason: "dose_evidence_text_match" });

expect(
decideTextFastPath(
"What medication doses are used for opioid withdrawal?",
[
result({ content: "Opioid withdrawal management guidance.", similarity: 0.9 }),
result({ content: "For agitation, lorazepam 1 mg IM may be used.", similarity: 0.88 }),
],
"medication_dose_risk",
),
).toEqual({ returnFastPath: false, reason: "missing_dose_query_context" });
});

it("keeps flowchart zone-action fast paths gated on action evidence", () => {
Expand Down