From dec7c34ce9a9ab9ca677591769322386b7f520d9 Mon Sep 17 00:00:00 2001 From: kaminimangal <128952805+kaminimangal@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:17:47 +0530 Subject: [PATCH] Surface dropped_citations in /ask API response and CLI --- tests/test_api.py | 20 ++++++++++++++++++++ vaultrag/cli.py | 4 ++++ vaultrag/main.py | 2 ++ 3 files changed, 26 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 3f70e17..f25d074 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -119,6 +119,26 @@ async def test_ask_returns_only_permitted_documents(client): cited = {c["doc_id"] for c in body["citations"]} assert "ceo-private" not in cited, "LEAK: CEO notes surfaced over the API" +async def test_dropped_citations_are_surfaced_in_ask_response(client): + """The model cites source [9] when only one source (index 1) was actually given. + + The dropped citation should be visible in the API response, not silently discarded. + """ + await _seed(client) + from vaultrag.generate import FakeLLM + client._transport.app.state.llm = FakeLLM( + '{"answer": "the bonus is 10%", "cited": [1, 9], "conflict": false}' + ) + r = await client.post( + "/ask", + json={"question": "quarterly bonus payout policy"}, + headers={"X-User-Id": "alice"}, + ) + assert r.status_code == 200 + body = r.json() + assert body["answered"] is True + assert body["dropped_citations"] == [9] + async def test_user_with_no_access_gets_a_refusal_not_a_leak(client): """bob is in sales. Neither seeded document is his, so he should be told nothing was found, diff --git a/vaultrag/cli.py b/vaultrag/cli.py index e02a6b1..863c00c 100644 --- a/vaultrag/cli.py +++ b/vaultrag/cli.py @@ -91,6 +91,10 @@ async def _ask(args) -> int: console.print(f"[yellow]stale[/] {s.doc_id} last updated {s.age_days}d ago (owner: {s.owner})") answer = generate(llm, args.question, hits) + + if answer.dropped_citations: + console.print(f"[yellow]unverified citations dropped[/] {answer.dropped_citations}") + console.print(f"\n[bold]{answer.text}[/]") if answer.citations: console.print("[dim]sources: " + ", ".join(c.doc_id for c in answer.citations) + "[/]") diff --git a/vaultrag/main.py b/vaultrag/main.py index 707df80..65b71ac 100644 --- a/vaultrag/main.py +++ b/vaultrag/main.py @@ -49,6 +49,7 @@ class AskResponse(BaseModel): citations: list[CitationOut] = [] conflict: bool = False refusal_reason: str | None = None + dropped_citations: list[int] = [] query_id: int @@ -161,6 +162,7 @@ async def ask(req: AskRequest, user_id: str = Depends(current_user)) -> AskRespo answered=answer.answered, conflict=answer.conflict, refusal_reason=answer.refusal_reason, + dropped_citations=answer.dropped_citations, query_id=query_id, citations=[ CitationOut(doc_id=c.doc_id, title=c.title, chunk_id=c.chunk_id, url=c.url, owner=c.owner)