diff --git a/server/_core/embeddings/degradation-alert.ts b/server/_core/embeddings/degradation-alert.ts index 50a2d45f..e530cbd4 100644 --- a/server/_core/embeddings/degradation-alert.ts +++ b/server/_core/embeddings/degradation-alert.ts @@ -60,3 +60,37 @@ export function alertSilentEmptyResult(detail: SilentEmptyDetail): void { 'error', ); } + +/** Drug/dosing keyword fallback for queries the intent classifier misses. */ +const CLINICAL_DRUG_KEYWORDS = /\b(dose|doses|dosing|dosage|mg|mcg|administer|give|max|drug|medication)\b/i; + +export interface ClinicalQuerySignal { + /** NormalizedQuery.intent */ + intent: string; + /** NormalizedQuery.isEmergent — cardiac arrest, anaphylaxis, airway, etc. */ + isEmergent: boolean; + /** NormalizedQuery.extractedMedications */ + extractedMedications: string[]; + /** NormalizedQuery.extractedConditions — chest pain, stroke, seizure, etc. */ + extractedConditions: string[]; + /** NormalizedQuery.normalized text */ + normalizedText: string; +} + +/** + * True if a query is clinically meaningful, so a zero-result on a resolved + * agency is worth alerting (the "blank screen mid-call" case). Broader than + * dosing alone: a paramedic searching "chest pain" / "stroke" / "cardiac arrest" + * and getting nothing is exactly the failure we must surface. Reuses the + * normalizer's intent / emergent / condition / medication signals. + */ +export function isClinicalQuery(s: ClinicalQuerySignal): boolean { + return ( + s.intent === 'medication_dosing' || + s.intent === 'contraindication_check' || + s.isEmergent || + s.extractedMedications.length > 0 || + s.extractedConditions.length > 0 || + CLINICAL_DRUG_KEYWORDS.test(s.normalizedText) + ); +} diff --git a/server/routers/search/semantic.ts b/server/routers/search/semantic.ts index 1a7b8de5..15a6f7fe 100644 --- a/server/routers/search/semantic.ts +++ b/server/routers/search/semantic.ts @@ -24,7 +24,7 @@ import { validateSearchLimit, getUserTierFeatures } from "../../_core/tier-valid import { toStateCode } from "../../lib/state-codes"; import { incrementAndCheckQueryLimit } from "../../db/users-usage"; import { logger } from "../../_core/logger"; -import { alertSilentEmptyResult } from "../../_core/embeddings/degradation-alert"; +import { alertSilentEmptyResult, isClinicalQuery } from "../../_core/embeddings/degradation-alert"; import { logQuery, createQueryLogEntry } from "../../_core/query-analytics"; import { getDrugByName } from "../../db/drugs"; import { @@ -282,7 +282,15 @@ export const semanticRouter = router({ query: input.query, agencyId, agencyName, - isClinical: isDrugRelated || isMedicationQuery, + // Broadened beyond dosing: also alert on emergent / condition queries + // (chest pain, stroke, cardiac arrest) that blank-screen mid-call. + isClinical: isClinicalQuery({ + intent: normalized.intent, + isEmergent: normalized.isEmergent, + extractedMedications: normalized.extractedMedications, + extractedConditions: normalized.extractedConditions, + normalizedText: normalized.normalized, + }), }); } diff --git a/tests/embedding-degradation-alert.test.ts b/tests/embedding-degradation-alert.test.ts index 27087ca9..0dd89dad 100644 --- a/tests/embedding-degradation-alert.test.ts +++ b/tests/embedding-degradation-alert.test.ts @@ -3,6 +3,7 @@ import * as sentry from "../server/_core/sentry"; import { alertEmbeddingDegraded, alertSilentEmptyResult, + isClinicalQuery, } from "../server/_core/embeddings/degradation-alert"; /** @@ -49,3 +50,34 @@ describe("silent-degradation alerts", () => { expect(spy).not.toHaveBeenCalled(); }); }); + +describe("isClinicalQuery (broadened clinical classification)", () => { + const base = { + intent: "general_search", + isEmergent: false, + extractedMedications: [] as string[], + extractedConditions: [] as string[], + normalizedText: "", + }; + + it("is true for an EMERGENT query with no drug term (chest pain / cardiac arrest)", () => { + expect(isClinicalQuery({ ...base, isEmergent: true, normalizedText: "cardiac arrest" })).toBe(true); + }); + + it("is true for a CONDITION query (stroke) even without dosing keywords", () => { + expect(isClinicalQuery({ ...base, extractedConditions: ["stroke"], normalizedText: "stroke" })).toBe(true); + }); + + it("is true for medication_dosing intent and for extracted medications", () => { + expect(isClinicalQuery({ ...base, intent: "medication_dosing", normalizedText: "epi dose" })).toBe(true); + expect(isClinicalQuery({ ...base, extractedMedications: ["epinephrine"], normalizedText: "epinephrine" })).toBe(true); + }); + + it("is true via the dosing-keyword fallback the intent classifier might miss", () => { + expect(isClinicalQuery({ ...base, normalizedText: "how much to administer" })).toBe(true); + }); + + it("is FALSE for a non-clinical / navigational query", () => { + expect(isClinicalQuery({ ...base, normalizedText: "about page settings" })).toBe(false); + }); +});