feat(citations): add citation merging to tests and tooling (closes #934) - #1019
Open
arcgod-design wants to merge 1 commit into
Open
feat(citations): add citation merging to tests and tooling (closes #934)#1019arcgod-design wants to merge 1 commit into
arcgod-design wants to merge 1 commit into
Conversation
…DarshanGK#934) Adds a zero-dependency citation-merging utility to backend/services/citation_utils.py that combines multiple citation lists (e.g., from two retrieval batches) into a single deduplicated, aggregated list: - merge_citations(*lists, score_merger, preview_merger, scores) Dedup by canonical (source, chunk) key. Each merged entry has: - occurrence_count: total times this citation was seen - contributors: indices of input lists that contained it - preview: combined via callable (longest wins by default) - score: combined via callable (arithmetic mean by default) - MergeResult exposes {merged, duplicates, contributed_by} with .to_dict() for JSON serialisation. - normalize_citations(list) helper coerces heterogenous inputs (dicts, legacy strings, malformed) to canonical shape. Conflict policy: first-seen entry wins structure fields (source, chunk and any ad-hoc fields); preview and score are aggregated via the pluggable callables. Custom score/preview mergers supported for experiments. New tooling: scripts/merge_citations.py — CLI that reads N JSON files (each a JSON array of dicts or strings), optionally parallel scores, and emits JSON or a human-readable report. Exit codes: 0/1/2 to match the repo's other tooling conventions. New tests: backend/tests/test_citation_merging.py — 45 pytest cases: - TestDefaultScoreMerger (7): empty, single, mean, None-skip, non- numeric skip, partial None. - TestDefaultPreviewMerger (6): empty, single, longest wins, tie, None skip. - TestMergeCitationsBasic (6): no-inputs, single, disjoint, overlap dedupe, different chunks kept, ad-hoc fields preserved. - TestMergeOrder (2): first-seen order, duplicates don't promote. - TestLegacyStringCompatibility (3): string promotion, mixed lists, duplicate strings. - TestScoreAggregation (5): set from scores, aggregated on duplicate, short scores padded, explicit None, no-score path. - TestCustomMergers (2): max() score merger, first_seen preview. - TestEdgeCases (6): empty list arg, non-list skip, malformed normalised, non-numeric chunk, three-way merge partial overlap, preview empty. - TestNormalizeCitations (5): empty, shallow copy, string promoted, non-list, mixed entries. - TestMergeResultSerialisation (2): to_dict shape, empty path. - TestBuildSourcesInterplay (1): merge two build_sources outputs. Backward-compatible: build_sources signature/behavior unchanged, the 21 existing test_citations.py cases still pass unmodified. Acceptance criteria from issue imDarshanGK#934: [x] Implement the change in the tests and tooling [x] Add or update tests, docs, or validation for the touched path
|
@arcgod-design is attempting to deploy a commit to the Darshan's projects Team on Vercel. A member of the Team first needs to authorize it. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

What
Adds a citation-merging utility to the citation-utils module and tests/tooling — closing issue #934's "Add citation merging to tests and tooling".
When two retrieval batches return overlapping citations (e.g. a streaming-resume retry that re-asks alongside a fresh batch), the assistant currently surfaces the same source twice.
merge_citations()dedupes by canonical(source, chunk)key, aggregates per-citation occurrence counts and scores, and emits a provenance audit so a reviewer can trace which input lists contributed which citations.Why
With streaming resume (
routes/chat.pyalready retries on transient errors), it's possible for a second retrieval pass to return the exact same chunk that the first pass returned. Currently both appear insources[]— a UX wart that also skews retrieval count metrics. Issue #933 added per-source ranking; this PR adds the symmetric merge operation: combine multiple citation lists into one canonical list while preserving fidelity (occurrence count, aggregated score, longest preview).What changed
Backend service —
backend/services/citation_utils.pyThe existing
build_sources()function (43 lines, issue #318-era) is unchanged. New helpers appended below it:default_score_merger(scores)— arithmetic mean; None entries skipped (no division-by-zero); non-numeric skipped. Mirrors Add source ranking to tests and tooling #933's relevance-collapse-to-neutral semantics.default_preview_merger(previews)— longest-wins (ties broken by earliest-seen order)._normalise_source_entry(entry)— promote legacy strings to{"source": str, "chunk": 0}; coercechunkto int ("3"→3,"garbage"→0) so a malformed upstream entry doesn't break_merge_key._merge_key(source)— returns(str(source), int(chunk))tuple.MergeResultdataclass —merged(canonical list),duplicates(audit list of dropped entries),contributed_by(mapping dedupe-key → list of input-list indices)..to_dict()for JSON serialisation.merge_citations(*citation_lists, score_merger, preview_merger, scores)— variadic merge:(source, chunk):source&chunktaken from first-seen entry (deterministic).previewaggregated via configurablepreview_merger(longest-wins default).scoreaggregated via configurablescore_merger(arithmetic-mean default).occurrence_countandcontributors[]list updated.metadata.created_at) preserved from first-seen entry only — no silently mixing two dicts' nested fields.scoresparameter is a parallel list-of-(list|None); length auto-padded with None to matchcitation_listslength.normalize_citations(citations)— convenience wrapper around_normalise_source_entryfor callers that want a list of canonical dicts without invoking the merge.Tooling —
scripts/merge_citations.pyOffline CLI that:
--scores file1 [file2|none] file3.MergeResult.to_dict()) or a--plainreport.0success,1bad JSON shape,2file-not-found. Mirrors the conventions established byscripts/rank_sources.py(Add source ranking to tests and tooling #933) andscripts/dedupe_docs.py(Add dedupe logic to build and deploy docs #928).Example:
Tests —
backend/tests/test_citation_merging.py45 pytest cases, all passing:
TestDefaultScoreMergerTestDefaultPreviewMergerTestMergeCitationsBasicTestMergeOrderTestLegacyStringCompatibilityTestScoreAggregationTestCustomMergersmax()score merger, first-seen preview mergerTestEdgeCasesTestNormalizeCitationsTestMergeResultSerialisationto_dictshape, empty pathTestBuildSourcesInterplaybuild_sourcesoutput →merge_citationsproduces deduped + aggregated resultExisting
test_citations.py(21 cases) still passes unmodified — backward-compatible.Verification
Acceptance criteria
Closes #934.