-
Notifications
You must be signed in to change notification settings - Fork 0
fix(rag): don't rescue bare cross-reference answers in source-backed recovery #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
74d2226
fix(rag): don't rescue bare cross-reference answers in source-backed …
BigSimmo 1226d6f
Merge branch 'main' into claude/rag-cross-reference-guard
BigSimmo bd41d4f
Merge branch 'main' into claude/rag-cross-reference-guard
BigSimmo abb648e
fix(rag): require a document-style object in the cross-reference guard
BigSimmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { | ||
| finalizeRagAnswerQuality, | ||
| generatedAnswerQualityFailureReason, | ||
| isBareCrossReferenceAnswer, | ||
| sourceBackedGenerationTimeoutAnswer, | ||
| } from "../src/lib/rag-extractive-answer"; | ||
| import type { Citation, RagAnswer, RagQueryClass, RetrievalSelectionSummary } from "../src/lib/types"; | ||
|
|
||
| // The exact off-topic extract observed live for "What should discharge documentation include?" — | ||
| // a bare cross-reference that trips missing_query_overlap and was being wrongly rescued to grounded | ||
| // on the strength of the cited sources' structured-chunk signals. | ||
| const RKPG_CROSS_REFERENCE = | ||
| "Refer to the RKPG Guidelines to Writing for Clinical Policy for further information about Scope of Practice."; | ||
| // The legitimate terse paraphrase the recovery gate exists for (regression guard from | ||
| // rag-answer-fallback.test.ts): shares no literal query token yet genuinely answers the query. | ||
| const LAI_PARAPHRASE = "Depot antipsychotic follow-up is covered by the cited local pathway."; | ||
|
|
||
| function citation(overrides: Partial<Citation> = {}): Citation { | ||
| return { | ||
| chunk_id: "discharge-1", | ||
| document_id: "discharge-doc", | ||
| title: "Admission to Discharge for Mental Health Inpatients", | ||
| file_name: "Admission to Discharge for Mental Health Inpatients (NMHS).pdf", | ||
| page_number: 3, | ||
| chunk_index: 0, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| // A source selection strong enough that, absent the cross-reference guard, the recovery gate would | ||
| // preserve the answer: required signals satisfied, a specific source signal, and structured chunks. | ||
| function strongSourceSelection(): RagAnswer["smartApiPlan"] { | ||
| const sourceSelection = { | ||
| candidateCount: 6, | ||
| selectedCount: 4, | ||
| requiredSignalsSatisfied: true, | ||
| matchedSignals: ["document_title", "patient_education", "table"], | ||
| missingRequiredSignals: [], | ||
| rescueApplied: false, | ||
| topChunkTypes: { text: 2, table: 1, flowchart: 0, medication_chart: 0, patient_education: 4 }, | ||
| } satisfies RetrievalSelectionSummary; | ||
| // Only answerPlan.sourceSelection drives the recovery gate; the rest of the plan is irrelevant | ||
| // here, so cast past the unrelated required fields rather than fabricating them. | ||
| return { answerPlan: { sourceSelection } } as unknown as RagAnswer["smartApiPlan"]; | ||
| } | ||
|
|
||
| function groundedExtractiveAnswer(text: string, overrides: Partial<RagAnswer> = {}): RagAnswer { | ||
| return { | ||
| answer: text, | ||
| grounded: true, | ||
| confidence: "low", | ||
| citations: [citation()], | ||
| sources: [], | ||
| routingMode: "extractive", | ||
| routingReason: "high_confidence_extractive_retrieval", | ||
| smartApiPlan: strongSourceSelection(), | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| describe("isBareCrossReferenceAnswer", () => { | ||
| it("flags a bare 'refer to <document> for further information' redirect", () => { | ||
| expect(isBareCrossReferenceAnswer(RKPG_CROSS_REFERENCE)).toBe(true); | ||
| }); | ||
|
|
||
| it("flags redirect-first and 'please see … for more detail' phrasings", () => { | ||
| expect( | ||
| isBareCrossReferenceAnswer( | ||
| "For further information about discharge planning, see the National Mental Health policy.", | ||
| ), | ||
| ).toBe(true); | ||
| expect(isBareCrossReferenceAnswer("Please see the local operational policy for more detail.")).toBe(true); | ||
| }); | ||
|
|
||
| it("sees through markdown bolding on the pointer", () => { | ||
| expect(isBareCrossReferenceAnswer(`**${RKPG_CROSS_REFERENCE}**`)).toBe(true); | ||
| }); | ||
|
|
||
| it("does NOT flag a terse paraphrase that merely lacks literal query tokens", () => { | ||
| expect(isBareCrossReferenceAnswer(LAI_PARAPHRASE)).toBe(false); | ||
| }); | ||
|
|
||
| it("does NOT flag the source-pointer generation-timeout fallback", () => { | ||
| expect( | ||
| isBareCrossReferenceAnswer(sourceBackedGenerationTimeoutAnswer("What is the clozapine ANC threshold?")), | ||
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("does NOT flag a real answer whose lead carries content and only a trailing sentence points onward", () => { | ||
| expect( | ||
| isBareCrossReferenceAnswer( | ||
| "Discharge documentation should include a mental state examination, risk assessment and follow-up plan. Refer to the NMHS policy for more detail.", | ||
| ), | ||
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("does NOT flag an ordinary clinical directive", () => { | ||
| expect(isBareCrossReferenceAnswer("Withhold clozapine and repeat the full blood count within 24 hours.")).toBe( | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| it("does NOT flag passive clinical referral facts that point at a service, not a document", () => { | ||
| // A directive + redirect that targets a team/person is a valid cited answer, not a document | ||
| // cross-reference — so it must survive the guard (Codex review PR #538). | ||
| expect( | ||
| isBareCrossReferenceAnswer("Patients are referred to the community team for further information and support."), | ||
| ).toBe(false); | ||
| expect(isBareCrossReferenceAnswer("The patient should see their GP for further information.")).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("source-backed recovery gate — cross-reference guard", () => { | ||
| const DISCHARGE_QUERY = "What should discharge documentation include?"; | ||
| const LAI_QUERY = "What is the long acting injectable pathway?"; | ||
| const queryClass = "broad_summary" satisfies RagQueryClass; | ||
|
|
||
| it("classifies the RKPG cross-reference as missing_query_overlap (the reachable-by-recovery reason)", () => { | ||
| expect( | ||
| generatedAnswerQualityFailureReason(groundedExtractiveAnswer(RKPG_CROSS_REFERENCE), DISCHARGE_QUERY, queryClass), | ||
| ).toBe("missing_query_overlap"); | ||
| }); | ||
|
|
||
| it("does NOT rescue the off-topic cross-reference despite strong structured-chunk signals", () => { | ||
| const answer = finalizeRagAnswerQuality( | ||
| groundedExtractiveAnswer(RKPG_CROSS_REFERENCE), | ||
| DISCHARGE_QUERY, | ||
| queryClass, | ||
| ); | ||
|
|
||
| expect(answer.grounded).toBe(false); | ||
| expect(answer.routingReason).toContain("final_quality_gate:missing_query_overlap"); | ||
| expect(answer.routingReason).not.toContain("source_backed_recovery"); | ||
| expect(answer.answer).not.toMatch(/RKPG|Refer to the/i); | ||
| }); | ||
|
|
||
| it("still rescues a legitimate source-backed paraphrase with the same strong signals (regression guard)", () => { | ||
| const answer = finalizeRagAnswerQuality(groundedExtractiveAnswer(LAI_PARAPHRASE), LAI_QUERY, queryClass); | ||
|
|
||
| expect(answer.grounded).toBe(true); | ||
| expect(answer.confidence).toBe("medium"); | ||
| expect(answer.routingReason).toContain("final_quality_gate_source_backed_recovery:missing_query_overlap"); | ||
| expect(answer.answer.replace(/\*\*/g, "")).toMatch(/Depot antipsychotic follow-up/i); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.