fix(consolidation): eliminate duplicate observations (interleave dedup recall + tool & benchmark)#1907
Merged
Conversation
3cb2942 to
8a1f322
Compare
Enumerates a bank's observations via the API, re-embeds locally, and clusters near-duplicates by cosine similarity. Includes unit tests and a registered 'find-duplicate-observations' script.
Consolidation produced duplicate observations two ways: (1) the cross-encoder reranker demoted a near-identical existing observation below the recall budget, hiding it from the LLM; (2) even when shown the twin, the model emitted an identical CREATE anyway (often while also UPDATE-ing it). Changes: - recall: add a 'rerank' flag; consolidation recall ranks by RRF (rerank=False) so a near-identical existing observation stays near the top instead of being demoted by the cross-encoder. Default recall path is unchanged. - consolidation: drop a CREATE whose normalized text exactly matches an observation shown to the LLM, or the text of an UPDATE issued in the same response (exact match => no information loss). See _duplicate_create_target; dropped creates are logged at WARNING. - add a 'reason' field to consolidation create/update/delete actions so the model explains its choices (surfaced when a duplicate create is dropped). - tests/eval: deterministic unit test for the dedup guard (test_consolidation_dedup.py); the duplicate-observation rate is tracked by a new quality benchmark (hindsight-dev/benchmarks/obs) over a synthetic herb-garden transcript and the real PII-free hermes session that surfaced the bug, reusing the obs-dedup tool — instead of a flaky real-LLM pytest.
The benchmark's explicit run_consolidation_job drain loop raced the default BrokerTaskBackend's worker poller (which runs retain's auto-submitted consolidation and the consolidator's own round-limit re-submit), leaving facts with both consolidated_at and consolidation_failed_at. Use SyncTaskBackend so all consolidation runs inline/serially and the drain loop is the sole consolidator; additionally disable auto-consolidation per-bank to confine consolidation to the drain loop for a clean measurement point. Add --fraction (scale up 1/4 -> full incrementally), --dataset (filter), and --wipe-bank (default now persists the bank for inspection) flags.
… always shown The residual near-duplicate observations were recall misses, not LLM errors: the existing near-identical observation (the merge 'twin') is semantic rank #1 for the new fact, but shares no source-fact graph link and little lexical overlap, so RRF averaged it below the per-fact recall budget (512 tokens, ~8 observations) and the LLM never saw it -> created a duplicate. The cross-encoder demoted it the same way (semantic #1 -> reranked #37). Add round-robin interleave fusion: take each retrieval arm's #1, then each #2, ... (semantic, bm25, graph, temporal), de-duplicating, until the budget fills. This guarantees every arm's top hit a slot, so the semantic-#1 twin is always shown and the LLM UPDATEs instead of creating a duplicate. Replace the recall 'rerank: bool' / ad-hoc passthrough with a single named 'reranking' strategy: 'cross_encoder' (default), 'rrf', 'interleave'. Consolidation dedup recall uses 'interleave'. For interleave the fusion order is authoritative — combined scoring's recency/temporal re-sort is skipped (that re-sort is what buried the twin). Measured on an English translation of the hermes transcript (removes the cross-lingual embedding confound): near-dup observations 4% (@0.97) -> 0% at both 1/10 and 1/4 cuts, coverage 89% -> 94%, no false merges (heavy observations are coherent single themes; distinct facets stay separate).
…dataset Wire the observation-dedup benchmark into the continuous-performance-monitor like perf-test/LoComo: - obs_benchmark.py: add --output and a headline overall_duplication_rate (near-dup redundant / total observations at the strictest threshold). - scripts/benchmarks/publish-obs-results.sh: enrich a run with commit/workflow metadata and push data/obs/<run>.json + data/obs-index.json to the dashboard repo's gh-pages (mirrors publish-locomo-results.sh). - perf-test.yml: add an 'obs' job (VertexAI, embedded pg0, serial SyncTaskBackend) that runs the benchmark and publishes; workflow_dispatch inputs obs_skip/ obs_dataset/obs_fraction. Defaults to the English hermes transcript at full fraction — a clean, deterministic signal (the Chinese variant adds a cross-lingual embedding confound). - Add the English-translated hermes dataset (the monolingual signal the interleave fix was validated on). Dashboard frontend (obs.html + nav) lives in the hindsight-continuous-performance-monitor repo.
_norm_obs_text dropped a CREATE whose text matched a shown observation only after lower-casing. But the guard's premise is that an exact-text match loses no info on drop — case-folding breaks that (a create differing only in case, e.g. an acronym like "TLS" vs "tls", carries different information). Collapse whitespace only; keep case. The guard body stays — it's a zero-cost deterministic backstop for the verbatim-re-create mode, complementary to interleave recall (which surfaces the twin; the guard catches the LLM emitting an identical CREATE anyway).
791f0fc to
4d226da
Compare
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.
Problem
Consolidation could emit duplicate observations — multiple near-identical (sometimes byte-identical) observations of the same fact within a single document/session.
Root cause (investigated end-to-end)
Two distinct mechanisms, confirmed with the new per-bank LLM tracing (#1922):
CREATEwhose text exactly matches an observation already shown to it (or anUPDATEin the same response).consolidation_max_tokens=512, ~8 observations) and the LLM never saw it → created a duplicate. The cross-encoder demoted it the same way (semantic Fix dependencies and docker #1 → reranked async operations error #37).Verified on traced runs: for every residual near-dup, the twin was semantic #1 (cosine 0.84–0.88) yet absent from the consolidation prompt; the LLM's own
reasonsaid "not covered by existing observations".Fix
UPDATEs instead of creating a duplicate.rerankingstrategy param. Replaced the recallrerank: bool+ ad-hoc passthrough with a single named strategy:cross_encoder(default, unchanged for user-facing recall),rrf,interleave. Consolidation usesinterleave. For interleave the fusion order is authoritative — the recency/temporal re-sort is skipped (that re-sort is part of what buried the twin).CREATEwhose normalised text matches an observation shown to the LLM (or an in-responseUPDATE) is dropped (no info loss).reasonfield on consolidation actions (LLM explains each create/update/delete) — kept for observability.Results
Measured on an English translation of the hermes transcript (removes a cross-lingual embedding confound: the default
bge-small-encan't align Chinese facts ↔ English observations, which inflated loose near-dups):No false merges: the heavy observations are coherent single themes, and distinct facets (per-language targets, plan phases) stay as separate observations.
Also included
hindsight-devobservation-dedup tool (obs_dedup) — re-embeds observations, cosine-clusters near-dups; reused by the benchmark.benchmarks/obsquality benchmark — ingests a dataset, drains consolidation (serialSyncTaskBackendso the drain loop is the sole consolidator), and reports the duplication rate (exact + @0.97/@0.92).--fraction/--dataset/--wipe-bankflags.Testing
tests/test_interleave_fusion.py— round-robin order, dedup, semantic-top-hit-always-first, strictly-decreasing scores.tests/test_consolidation_dedup.py— the reconciliation guard.cross_encoder); existing recall/scoring tests pass.