From 3dfab9002d367f8d1e84ab1658e253d87f588351 Mon Sep 17 00:00:00 2001 From: MohammadSyed18 <79668964+MohammadSyed18@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:51:07 -0500 Subject: [PATCH] Add evidence-aware retrieval responses --- README.md | 3 +-- src/index.js | 52 ++++++++++++++++++++++++++++++++++++---------- test/index.test.js | 15 +++++++++++++ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3c73b85..712e655 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ BridgeAI helps families turn scattered school announcements into sourced answers, deadlines, and next steps. It indexes announcements locally, retrieves the best supporting passages, extracts action items, and refuses to sound certain when the source coverage is weak. -Current state: ingestion, ranked retrieval, citations, confidence scoring, deadline extraction, English/Spanish query normalization, HTTP API, demo UI, and tests are working. Live district connectors are intentionally not included yet. +Current state: ingestion, ranked retrieval, sentence-level evidence mapped to citations, conflicting-date detection, confidence scoring, deadline extraction, English/Spanish query normalization, HTTP API, demo UI, and tests are working. Live district connectors are intentionally not included yet. ```bash npm test npm start # open http://localhost:8080 ``` - diff --git a/src/index.js b/src/index.js index 05e0330..cabdc88 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import {extractActions, normalize, sentences} from "./text.js"; +import {extractActions, extractDates, normalize, sentences} from "./text.js"; export class KnowledgeIndex { constructor() { @@ -69,27 +69,58 @@ export function answer(index, question, now = new Date()) { overlap: normalize(candidate.sentence).filter(token => queryTokens.has(token)).length })) .sort((a, b) => b.overlap - a.overlap || b.result.score - a.result.score) + .filter(candidate => candidate.overlap > 0) .slice(0, 3); - const citations = [...new Map(selected.map(({result}) => [result.documentId, { - id: result.documentId, - title: result.source.title, - url: result.source.url, - publishedAt: result.source.publishedAt, - passage: result.id - }])).values()]; + if (!selected.length) { + return {answer: "I could not find a source that answers that yet.", confidence: 0.08, citations: [], evidence: [], actions: [], conflicts: []}; + } + const citationByDocument = new Map(); + for (const {result} of selected) { + if (!citationByDocument.has(result.documentId)) citationByDocument.set(result.documentId, { + id: result.documentId, + title: result.source.title, + url: result.source.url, + publishedAt: result.source.publishedAt, + passage: result.id + }); + } + const citations = [...citationByDocument.values()]; + const citationNumber = new Map(citations.map((citation, index) => [citation.id, index + 1])); + const evidence = selected.map(({sentence, result}) => ({ + statement: sentence, + citation: citationNumber.get(result.documentId), + passage: result.id, + score: Number(result.score.toFixed(3)) + })); const coverage = Math.min(1, new Set(results.flatMap(result => result.tokens.filter(token => queryTokens.has(token)))).size / queryTokens.size); const corroboration = Math.min(1, new Set(results.map(result => result.documentId)).size / 2); const topStrength = Math.min(1, results[0].score / 5); - const confidence = Number((0.55 * coverage + 0.25 * corroboration + 0.2 * topStrength).toFixed(2)); + const conflicts = findDateConflicts(evidence); + const confidence = Number(Math.max(0.05, 0.55 * coverage + 0.25 * corroboration + 0.2 * topStrength - 0.2 * conflicts.length).toFixed(2)); const prefix = confidence < 0.45 ? "The available announcement suggests: " : "Based on the school announcements: "; return { - answer: prefix + selected.map(({sentence}, index) => `${sentence} [${index + 1}]`).join(" "), + answer: prefix + evidence.map(item => `${item.statement} [${item.citation}]`).join(" "), confidence, citations, + evidence, + conflicts, actions: extractActions(selected.map(item => item.sentence).join(" ")) }; } +function findDateConflicts(evidence) { + const dates = new Map(); + for (const item of evidence) { + for (const value of extractDates(item.statement)) { + const key = value.toLowerCase(); + if (!dates.has(key)) dates.set(key, new Set()); + dates.get(key).add(item.citation); + } + } + if (dates.size < 2) return []; + return [{type: "date", values: [...dates.entries()].map(([value, sources]) => ({value, citations: [...sources]}))}]; +} + function frequency(tokens) { const values = new Map(); for (const token of tokens) values.set(token, (values.get(token) ?? 0) + 1); @@ -101,4 +132,3 @@ function chunk(values, size) { for (let i = 0; i < values.length; i += size) output.push(values.slice(i, i + size)); return output; } - diff --git a/test/index.test.js b/test/index.test.js index ea9ae36..68f8f22 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -39,3 +39,18 @@ test("action extraction keeps associated dates", () => { assert.deepEqual(actions[0].dates, ["August 18, 2026"]); }); +test("each answer marker points to its supporting source and conflicts lower confidence", () => { + const index = fixture(); + index.add({ + id:"reminder", title:"Registration reminder", url:"https://example.test/reminder", + publishedAt:"2026-08-02", authority:.8, + body:"Families should register online. Registration closes August 20, 2026." + }); + const result = answer(index, "When does registration close?", new Date("2026-08-04")); + assert.equal(result.evidence.length, 2); + for (const item of result.evidence) { + assert.equal(item.passage.split("#")[0], result.citations[item.citation - 1].id); + } + assert.equal(result.conflicts[0].type, "date"); + assert.ok(result.confidence < .8); +});