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/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4668,6 +4668,7 @@ function buildTableOrVisualSourceLookupAnswer(args: { query: string; results: Se
return {
answer,
citationChunkIds: [source.id],
preformatted: true,
answerSections: [
{
heading: "Source match",
Expand Down Expand Up @@ -4700,6 +4701,7 @@ function buildDocumentSupportListAnswer(args: { query: string; results: SearchRe
: `I found ${names.length} indexed documents that support this query: ${names.slice(0, -1).join("; ")}; and ${names.at(-1)}.`;
return {
answer,
preformatted: true,
citationChunkIds: Array.from(
new Set(
documents.flatMap((document) =>
Expand Down Expand Up @@ -4812,6 +4814,7 @@ function buildExtractiveAnswer(args: {
sources: args.results,
modelUsed: null,
routingMode: "extractive",
preformatted: hasExtractedAnswer && Boolean((naturalAnswer as { preformatted?: boolean }).preformatted),
routingReason: args.routeReason,
queryClass: args.queryClass,
latencyTimings: args.timings,
Expand Down Expand Up @@ -5237,6 +5240,14 @@ function finalizeRagAnswerQuality(answer: RagAnswer, query: string, queryClass:
}

function finalizeRagAnswerQualityCore(answer: RagAnswer, query: string, queryClass: RagQueryClass): RagAnswer {
// Deterministic, template-built answers (document-support lists, table/visual source
// references) are well-formed by construction and carry no free-text clinical claims.
// The clinical-prose sanitizer/quality gate below is designed for model prose and would
// strip their document names (facility codes like "(NOCC)(AKG)" read as non-prose),
// turning a valid answer into garble that then fails the gate. Return them untouched.
if (answer.preformatted && answer.grounded) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve preformatted answers through the client renderer

When this path returns a document-list answer intact, the browser still re-sanitizes answer.answer: I checked ClinicalDashboard.tsx, where safeAnswerText is always built with sanitizeAnswerDisplayText(answer?.answer ?? "") and passed as safeAnswerText || answer.answer before NaturalLanguageAnswer calls primaryAnswerDisplayText, which also uses the display/prose sanitizers. For the fixed NOCC-style query the API answer now keeps (NOCC) (AKG), but the rendered answer can still strip or mangle that suffix before display, so the user-facing regression remains unless the renderer honors preformatted or skips display sanitization for it.

Useful? React with 👍 / 👎.

return answer;
}
const cleanedAnswer = sanitizeAnswerText(answer.answer);
const gapLikeAnswer =
/could not find enough clean|no relevant clinical source|no current source|cannot provide a clinical answer|cannot provide a source-backed clinical answer|nearby indexed passages|not strong enough to support a reliable answer|no specific\b.*\bcan be confirmed|do not contain indexed guidance|do not contain (?:specific\s+)?information|do not provide specific|no\b.*\bguidance\b.*\bincluded|defer to other sources/i.test(
Expand Down
5 changes: 5 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,11 @@ export type RagAnswer = {
queryClass?: RagQueryClass;
queryAnalysis?: ClinicalQueryAnalysis;
responseMode?: AnswerResponseMode;
// True for deterministic, template-built answers (document-support lists, table/visual
// source references) assembled from source metadata rather than model prose. These are
// well-formed by construction, so the clinical-prose sanitizer/quality gate — which would
// strip their document names (facility codes read as non-prose) — must be skipped.
preformatted?: boolean;
latencyTimings?: {
search_cache_hit?: boolean;
shared_cache_hit?: boolean;
Expand Down
88 changes: 88 additions & 0 deletions tests/rag-answer-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,94 @@ describe("RAG structured-output fallback", () => {
expect(answer.answer).not.toMatch(/level checks|renal review|baseline tests/i);
});

it("preserves parenthetical facility codes in the document-support list answer", async () => {
// Regression: the document-list answer is deterministic and pre-formatted, but the
// clinical-prose sanitizer used to strip facility-code suffixes like "(NOCC) (AKG)"
// (read as non-prose), mangling a valid answer into garble the quality gate then
// refused. The `preformatted` flag now exempts it from that sanitizer.
vi.stubEnv("OPENAI_API_KEY", "test-key");
vi.stubEnv("RAG_SEARCH_CACHE_TTL_MS", "0");
vi.stubEnv("RAG_ANSWER_CACHE_TTL_MS", "0");

const noccDocument = {
id: "nocc-doc",
title: "National Outcomes and Casemix Collection (NOCC) (AKG)",
file_name: "NOCC.pdf",
metadata: {
source_title: "National Outcomes and Casemix Collection",
publisher: "Local service",
jurisdiction: "Australia/WA",
document_status: "current",
clinical_validation_status: "approved",
extraction_quality: "good",
},
text_rank: 0.31,
};
const noccChunk = {
id: "nocc-doc-chunk-1",
document_id: "nocc-doc",
page_number: 1,
chunk_index: 0,
section_heading: "Outcome measures",
section_path: ["Outcome measures"],
content: "National Outcomes and Casemix Collection guidance for outcome measures completion.",
retrieval_synopsis: "NOCC source summary.",
image_ids: [],
text_rank: 0.42,
};
const rpc = vi.fn(async (name: string) => {
if (name === "match_documents_for_query") return { data: [noccDocument], error: null };
if (name === "match_document_lookup_chunks_text") return { data: [noccChunk], error: null };
if (name === "match_document_chunks_hybrid") {
return {
data: [
source({
id: "nocc-doc-chunk-1",
document_id: "nocc-doc",
title: "National Outcomes and Casemix Collection (NOCC) (AKG)",
file_name: "NOCC.pdf",
section_heading: "Outcome measures",
content: "National Outcomes and Casemix Collection guidance for outcome measures completion.",
similarity: 0.91,
hybrid_score: 0.93,
text_rank: 0.42,
}),
],
error: null,
};
}
if (name === "get_related_document_metadata") return { data: [], error: null };
return { data: [], error: null };
});

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

const { answerQuestionWithScope } = await import("../src/lib/rag");

const answer = await answerQuestionWithScope({
query: "What documents support outcome measures completion?",
ownerId: undefined,
logQuery: false,
skipCache: true,
});

expect(answer.routingMode).toBe("extractive");
expect(answer.grounded).toBe(true);
expect(answer.preformatted).toBe(true);
// The facility-code suffix survives intact (previously mangled to a dangling "(").
expect(answer.answer).toContain("(NOCC) (AKG)");
expect(answer.answer).not.toMatch(/\(\s*(?:;|$| Outcome)/);
});

it("does not promote lithium dosage headings or continuation fragments as the primary answer", async () => {
vi.stubEnv("OPENAI_API_KEY", "test-key");
vi.stubEnv("RAG_SEARCH_CACHE_TTL_MS", "0");
Expand Down
Loading