- Notifications
You must be signed in to change notification settings - Fork 0
fix(rag): recover grounded source-backed canary answers#603
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
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8f39ab4
fix(rag): recover source-backed clinical answers
BigSimmo 5638931
Merge remote-tracking branch 'origin/main' into codex/rag-canary-reco…
BigSimmo 711e758
fix(rag): limit routine recovery to blocked retrieval
BigSimmo c0eb3e7
Merge branch 'main' into codex/rag-canary-recovery
BigSimmo e476df7
Merge remote-tracking branch 'origin/main' into codex/rag-canary-reco…
BigSimmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -288,12 +288,16 @@ export function classifyAnswerIntent(query: string, queryClass: RagQueryClass): | ||
| if (hasResultActionSignal) return "red_result_action"; | ||
| if (/\b(?:dose|dosing|dosage|max(?:imum)?|mg|mcg|renal|eGFR|creatinine)\b/i.test(query)) return "dose"; | ||
| if (/\b(?:pathway|refer|referral|criteria|ect|electroconvulsive)\b/.test(normalized)) return "pathway_referral"; | ||
| // Retrieval classification and answer intent are different concerns. A | ||
| // document_lookup route can still ask for the document's clinical content | ||
| // (for example, "What should a safety plan include?"). Treat it as a source | ||
| // lookup only when the wording explicitly asks to find/open/select a source; | ||
| // otherwise the extractive path must select responsive clinical facts rather | ||
| // than reference-list lines that merely mention a guideline or procedure. | ||
| if ( | ||
| queryClass === "document_lookup" || | ||
| /\b(?:find|show|open|which)\b.*\b(?:document|guideline|procedure|policy|protocol|form|source|file)\b/.test( | ||
| normalized, | ||
| ) || | ||
| /\b(?:documentation|forms?|documents?|sources?|guidelines?|procedure|policy|protocol)\b/.test(normalized) | ||
| ) | ||
BigSimmo marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ) { | ||
| return "document_lookup"; | ||
| } | ||
| @@ -343,7 +347,7 @@ function answerIntentEvidencePattern(intent: AnswerIntent) { | ||
| case "document_lookup": | ||
| return /\b(?:document|guideline|procedure|policy|protocol|form|source|file|support|supports|covers|contains)\b/i; | ||
| default: | ||
| return /\b(?:assess|arrange|check|continue|review|treat|manage|monitor|refer|dose|risk|therapy|diagnos\w*)\b/i; | ||
| return /\b(?:assess|arrange|check|collaborat\w*|complete|conduct|continue|develop|diagnos\w*|document|dose|ensure|identify|include|incorporate|involve|link|manage|monitor|provide|record|refer|revise|review\w*|risk|share|therapy|treat|update)\b/i; | ||
| } | ||
| } | ||
| @@ -736,7 +740,7 @@ function factSupportsAnswerIntent( | ||
| if (/^what\s+is\b/i.test(query)) { | ||
| return /\b(?:is|are|means|defined|characteri[sz]ed|involves|refers\s+to)\b/i.test(text); | ||
| } | ||
| return /\b(?:assess|arrange|check|continue|review|treat|manage|monitor|refer|dose|risk|therapy|diagnos\w*)\b/i.test( | ||
| return /\b(?:assess|arrange|check|collaborat\w*|complete|conduct|continue|develop|diagnos\w*|document|dose|ensure|identify|include|incorporate|involve|link|manage|monitor|provide|record|refer|revise|review\w*|risk|share|therapy|treat|update)\b/i.test( | ||
| text, | ||
| ); | ||
| } | ||
| @@ -964,7 +968,10 @@ function sectionForFactKind(kind: ExtractedClinicalFactKind): Pick<AnswerSection | ||
| case "caveat": | ||
| return { heading: "Caveat", kind: "source_gap" }; | ||
| default: | ||
| return { heading: "Bottom line", kind: "bottom_line" }; | ||
| // "Bottom line" is intentionally rejected by the generated-answer | ||
| // template detector. Use the neutral display heading while preserving | ||
| // the semantic kind so deterministic facts do not fail their own gate. | ||
| return { heading: "Key point", kind: "bottom_line" }; | ||
| } | ||
| } | ||
| @@ -1790,26 +1797,21 @@ export function finalizeRagAnswerQuality( | ||
| queryClass: RagQueryClass, | ||
| verificationSources?: SearchResult[], | ||
| ): RagAnswer { | ||
| const qualityChecked = finalizeRagAnswerQualityCore(answer, query, queryClass); | ||
| return applyProviderLabels( | ||
| assessAndEnforceClaimSupport(finalizeRagAnswerQualityCore(answer, query, queryClass, verificationSources)), | ||
| applyNumericVerification(assessAndEnforceClaimSupport(qualityChecked), verificationSources), | ||
| ); | ||
| } | ||
| /** | ||
| * Finalizes an answer by applying quality gates, sanitizing content, and verifying numeric claims. | ||
| * Finalizes answer prose by applying textual quality gatesand sanitizing content. | ||
| * | ||
| * @param answer - The answer to validate and finalize | ||
| * @param query - The user query used to assess relevance and highlight clinical terms | ||
| * @param queryClass - The classification of the user query | ||
| * @param verificationSources - Optional sources used to verify numeric claims | ||
| * @returns The finalized RAG answer with validated content, sections, and confidence metadata | ||
| */ | ||
| function finalizeRagAnswerQualityCore( | ||
| answer: RagAnswer, | ||
| query: string, | ||
| queryClass: RagQueryClass, | ||
| verificationSources?: SearchResult[], | ||
| ): RagAnswer { | ||
| 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 | ||
| @@ -1886,12 +1888,9 @@ function finalizeRagAnswerQualityCore( | ||
| }) | ||
| .filter((section): section is Exclude<typeof section, null> => Boolean(section)); | ||
| return applyNumericVerification( | ||
| { | ||
| ...answer, | ||
| answer: boldHighYieldClinicalText(cleanedAnswer, query), | ||
| answerSections, | ||
| }, | ||
| verificationSources, | ||
| ); | ||
| return { | ||
| ...answer, | ||
| answer: boldHighYieldClinicalText(cleanedAnswer, query), | ||
| answerSections, | ||
| }; | ||
| } | ||
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 |
|---|---|---|
| @@ -904,6 +904,61 @@ function applyConfidenceGate( | ||
| }; | ||
| } | ||
| /** | ||
| * Allow a score-blocked routine document-content query to use the deterministic | ||
| * answer only when that answer independently passes the final safety gates. | ||
| * | ||
| * This is deliberately narrower than the normal extractive router: it cannot | ||
| * recover medication, threshold, comparison, broad-summary, complex, or weakly | ||
| * related queries. The retrieval diagnostic remains blocked so the UI still | ||
| * presents the recovered answer with low-trust guidance. | ||
| */ | ||
| function hasValidatedRoutineExtractiveRecovery(args: { | ||
| query: string; | ||
| queryClass: RagQueryClass; | ||
| results: SearchResult[]; | ||
| route: { mode: "unsupported" | "extractive" | "fast" | "strong"; reason: string }; | ||
| sourceBacked: boolean; | ||
| }) { | ||
| if ( | ||
| args.queryClass !== "document_lookup" || | ||
| args.route.mode !== "fast" || | ||
| args.route.reason !== "strong_routine_retrieval" || | ||
| !args.sourceBacked | ||
| ) { | ||
| return false; | ||
| } | ||
| const candidate = finalizeRagAnswerQuality( | ||
| buildExtractiveAnswer({ | ||
| query: args.query, | ||
| queryClass: args.queryClass, | ||
| results: args.results, | ||
| quoteCards: [], | ||
| documentBreakdown: [], | ||
| evidenceSummary: undefined, | ||
| sourceCoverage: undefined, | ||
| conflictsOrGaps: [], | ||
| visualEvidence: [], | ||
| bestSource: null, | ||
| smartPanel: undefined, | ||
| relatedDocuments: [], | ||
| routeReason: `${args.route.reason}; validated_routine_extractive_recovery`, | ||
| timings: undefined, | ||
| }), | ||
| args.query, | ||
| args.queryClass, | ||
| ); | ||
| return ( | ||
| candidate.grounded && | ||
| candidate.confidence !== "unsupported" && | ||
| candidate.citations.length > 0 && | ||
| candidate.responseMode !== "evidence_gap" && | ||
| !/final_quality_gate:/.test(candidate.routingReason ?? "") | ||
| ); | ||
| } | ||
| /** Clamp confidence. */ | ||
| function clampConfidence( | ||
| proposed: RagAnswer["confidence"] | undefined, | ||
| @@ -4164,7 +4219,26 @@ async function answerQuestionWithScopeUncoalesced( | ||
| results: answerInputResults, | ||
| answerMode: routeFromRouting.mode, | ||
| }); | ||
| const gatedRoute = applyConfidenceGate(routeFromRouting, queryClass, initialRetrievalDiagnostics); | ||
| const validatedRoutineExtractiveRecovery = | ||
| initialRetrievalDiagnostics.gateStatus === "blocked" && | ||
| hasValidatedRoutineExtractiveRecovery({ | ||
| query: args.query, | ||
| queryClass, | ||
| results: answerInputResults, | ||
| route: routeFromRouting, | ||
| sourceBacked: relevance.isSourceBacked, | ||
| }); | ||
| const routeBeforeConfidenceGate = validatedRoutineExtractiveRecovery | ||
| ? { | ||
| ...routeFromRouting, | ||
| mode: "extractive" as const, | ||
| model: null, | ||
| reason: `${routeFromRouting.reason}; validated_routine_extractive_recovery`, | ||
| } | ||
| : routeFromRouting; | ||
| const gatedRoute = validatedRoutineExtractiveRecovery | ||
| ? { route: routeBeforeConfidenceGate } | ||
| : applyConfidenceGate(routeBeforeConfidenceGate, queryClass, initialRetrievalDiagnostics); | ||
BigSimmo marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // In source-only mode (offline, or auto with no usable key) we never call the model. Route to | ||
| // the deterministic extractive path when evidence is usable, but preserve the confidence gate's | ||
| // "unsupported" decision so weak evidence still fails closed to a source-gap answer rather than | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When any supported claim contains a clinical value, this branch skips the previous raw answer/section scan and only verifies values that made it into
supportedClaims.claimInputsderives those claims viasplitClaims, which drops short snippets under eight characters, so a model section likeMaximum dose: 500 mg(body500 mg) can bypass numeric faithfulness whenever another longer supported numeric claim is present; before this change every section body was scanned directly. Please keep the claim-scoped check but also scan answer/section text for clinical value atoms that were not covered by a claim.Useful? React with 👍 / 👎.