Skip to content

Accept validated numeric evidence citations - #7

Merged
dk3yyyy merged 1 commit into
mainfrom
fix/reliable-citation-aliases
Jul 31, 2026
Merged

Accept validated numeric evidence citations#7
dk3yyyy merged 1 commit into
mainfrom
fix/reliable-citation-aliases

Conversation

@dk3yyyy

@dk3yyyy dk3yyyy commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Problem

The local llama3.2 chat path can retrieve relevant reviews but still return:

I could not produce an answer with citations that match the retrieved reviews.

The answer prompt required the model to reproduce long content-derived source IDs such as [review_ab12…]. Small local models commonly emit short numeric citations such as [1] instead. The validator interpreted that number as an invented source ID and rejected the whole answer.

Fix

  • present each retrieved review as a short numbered evidence record such as [1]
  • retain the exact stable source ID beside each record as validation metadata
  • map every numeric citation strictly back to its retrieved source ID
  • reject missing, invented, and out-of-range evidence numbers
  • continue accepting exact source-ID citations for backward compatibility
  • renumber displayed citations by first appearance while preserving the correct source order
  • update README and architecture wording to reflect alias-to-source validation

The fail-closed citation policy remains unchanged: uncited answers and citations that do not map to retrieved evidence are not displayed.

Regression coverage

The new regression was observed failing on the merged code with the same citation-validation message shown in the macOS terminal screenshot. Coverage now includes:

  • valid numeric citation mapping
  • out-of-range numeric citation rejection
  • multiple sources cited in reverse retrieval order
  • exact source-ID citation compatibility
  • existing uncited and invented-source rejection

Verification

  • 43 tests passed on isolated CPython 3.11.15
  • 43 tests passed on isolated CPython 3.14.5
  • Ruff check and format passed
  • uv lock --check passed
  • git diff --check passed
  • uv build passed
  • architecture SVG pixel QA passed after the label update
  • independent exact-diff review returned APPROVE

Runtime validation still needed

The real macOS Ollama/llama3.2 path is not available on the development host. After CI, the branch should be pulled and tested with the original questions on the reporter's Mac before merge.

Greptile Summary

This PR fixes a regression where small local models (llama3.2) emit short numeric citations like [1] instead of long content-derived source IDs, causing the citation validator to reject valid answers. The fix presents each retrieved review as a numbered evidence record and maps numeric citations strictly back to their stable source IDs before validation, while retaining the exact source-ID citation path for backward compatibility.

  • _format_context now emits [{n}] + Source ID: {id} for each record; _validate_and_number_citations builds an evidence_aliases dict (e.g. {\"1\": \"review-abc…\"}) and resolves every cited token through it before checking against the retrieved set.
  • The fail-closed policy is unchanged: uncited answers, out-of-range numbers, and invented IDs all return the validation error message.
  • Four new regression tests cover numeric citation acceptance, multi-source reverse-order renumbering, out-of-range rejection, and the backward-compat source-ID path.

Confidence Score: 4/5

Safe to merge after runtime validation on the target macOS/Ollama host; the citation logic is correct for all realistic source ID shapes.

The alias-lookup line evidence_aliases.get(token, token) silently favors the evidence-number interpretation whenever a source ID is a pure decimal string that falls within the retrieved count, making backward-compat resolution for that document unreachable in those conditions. Content-derived IDs make this extremely unlikely in practice, but the precedence rule is implicit rather than explicit in the code.

Files Needing Attention: agent.py lines 106-110 — the evidence_aliases.get fallback logic warrants a clarifying comment or an explicit numeric vs. non-numeric branch to make alias precedence intentional rather than incidental.

Important Files Changed

Filename Overview
agent.py Core citation logic updated: _format_context now emits numbered evidence records with Source ID metadata, and _validate_and_number_citations builds an evidence_aliases mapping so numeric tokens resolve to source IDs before validation; backward compat for exact source-ID citations preserved via dict.get fallback. One edge case: numeric source IDs can silently collide with evidence alias keys.
tests/test_agent.py Four new regression tests added: valid numeric citation, multi-source reverse-order resolution, out-of-range rejection, and existing backward-compat source-ID test. First existing test updated to assert the new prompt format ([1] + Source ID line). Coverage is good for the new paths.
README.md Two wording updates to reflect the alias-to-source citation model; no functional changes.
docs/architecture.md Answer layer description updated to describe numbered evidence records and backward-compat source-ID acceptance; accurate to the new implementation.
docs/architecture.svg Single label change in the LOCAL OLLAMA node from 'source-ID citations' to 'validated citations'; cosmetic only.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Model answer] --> B{CITATION_PATTERN.findall}
    B -->|no citations| C[Return None → CITATION_VALIDATION_MESSAGE]
    B -->|cited_tokens list| D[For each token]
    D --> E{token in evidence_aliases?}
    E -->|yes — numeric citation| F[Resolve to alias source_id]
    E -->|no — source-ID citation| G[Use token as source_id directly]
    F --> H{source_id in retrieved?}
    G --> H
    H -->|no| C
    H -->|yes| I[Append to resolved_ids]
    I --> J{More tokens?}
    J -->|yes| D
    J -->|no| K[Deduplicate → ordered_ids]
    K --> L[Build citation_numbers map]
    L --> M[CITATION_PATTERN.sub — renumber]
    M --> N[Return numbered_answer + sources tuple]
Loading
Prompt To Fix All With AI
### Issue 1
agent.py:106-110
**Numeric source ID shadows evidence alias**

`evidence_aliases.get(token, token)` always returns the alias-mapped source ID when `token` is a decimal string that falls within the count of retrieved documents. If a source ID happens to be a pure decimal string (e.g. `"3"`) and the model emits `[3]` intending backward-compat citation of that document, the lookup resolves instead to the _third_ retrieved record — a different document — whenever three or more records were fetched. Additionally, the resolved document changes when the number of retrieved results crosses the threshold, making behaviour query-dependent. The scenario is unlikely given content-derived IDs, but worth a comment or guard (e.g. `if token.isdigit(): ... else: ...`) to make the precedence rule explicit.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Accept validated numeric evidence citati..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Comment thread agent.py
Comment on lines +106 to +110
for token in cited_tokens:
source_id = evidence_aliases.get(token, token)
if source_id not in retrieved:
return None
resolved_ids.append(source_id)

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 Numeric source ID shadows evidence alias

evidence_aliases.get(token, token) always returns the alias-mapped source ID when token is a decimal string that falls within the count of retrieved documents. If a source ID happens to be a pure decimal string (e.g. "3") and the model emits [3] intending backward-compat citation of that document, the lookup resolves instead to the third retrieved record — a different document — whenever three or more records were fetched. Additionally, the resolved document changes when the number of retrieved results crosses the threshold, making behaviour query-dependent. The scenario is unlikely given content-derived IDs, but worth a comment or guard (e.g. if token.isdigit(): ... else: ...) to make the precedence rule explicit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: agent.py
Line: 106-110

Comment:
**Numeric source ID shadows evidence alias**

`evidence_aliases.get(token, token)` always returns the alias-mapped source ID when `token` is a decimal string that falls within the count of retrieved documents. If a source ID happens to be a pure decimal string (e.g. `"3"`) and the model emits `[3]` intending backward-compat citation of that document, the lookup resolves instead to the _third_ retrieved record — a different document — whenever three or more records were fetched. Additionally, the resolved document changes when the number of retrieved results crosses the threshold, making behaviour query-dependent. The scenario is unlikely given content-derived IDs, but worth a comment or guard (e.g. `if token.isdigit(): ... else: ...`) to make the precedence rule explicit.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Evidence aliases intentionally take precedence for decimal bracket tokens. Application-generated source IDs are always content-derived with the review_ prefix, so a pure numeric source ID cannot occur in this runtime. Exact-ID backward compatibility therefore applies to the supported prefixed IDs; interpreting [3] as evidence record 3 is the unambiguous public contract. No code change needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Understood — with the review_ prefix as an invariant on all application-generated source IDs, a numeric collision is structurally impossible at runtime, so the precedence is safe and the public contract is clear.

Worth noting for anyone reading the code cold: a one-line comment such as

# Source IDs are always prefixed (e.g. "review_…"); plain decimal tokens are
# unambiguously evidence-alias references, so alias lookup takes intentional precedence.
source_id = evidence_aliases.get(token, token)

would make that invariant self-documenting without requiring any logic change. But that's optional — happy to close this if you'd rather leave it as-is.

Shall I remember this as a rule: "source IDs in this project always carry a review_ prefix, so numeric alias precedence in evidence_aliases.get is intentional and needs no guard"?

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@dk3yyyy
dk3yyyy merged commit b9800bd into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant