Skip to content

feat(grouper): strip noise + deterministic dedup before classification#31

Merged
belaytzev merged 2 commits into
mainfrom
feat/dedup-noise-reduction
May 9, 2026
Merged

feat(grouper): strip noise + deterministic dedup before classification#31
belaytzev merged 2 commits into
mainfrom
feat/dedup-noise-reduction

Conversation

@belaytzev

Copy link
Copy Markdown
Owner

Summary

Cuts digest duplication and noise by fixing three sources of low-signal output observed in production digests.

After Approach A (config-only: model upgrade + lower temp + tighter token budget) cut items 80→44, five problem categories remained. This PR targets all five:

Issue Fix
🚀 channel-recap header echoed as a bullet Strip leading 🚀 ... line before grouping
Section 2 (📎 Also) leaked into cross-channel groups Strip 📎 Also/Также section before grouping
Verbatim duplicate bullets within same group Deterministic dedup on (group, source, normalized_point) in _parse_grouped_response
Photo-only / meta-empty / expired-invite / admin / speculation posts surviving Explicit QUALITY GATE in base prompt + summarizer inline prompt
Section 1 unbounded growth Hard cap of 5 bullets

Why this design

Backend Architect review flagged that the grouper's single-pass design (extract + classify + dedup) is asking too much of the LLM at temperature 0.1 with 33 channels — it produces flakes. Prompt-only dedup_rule will not reliably catch verbatim duplicates. The right fix for that class is a deterministic chokepoint in _parse_grouped_response, which this PR adds.

A future PR can split the grouper into 2a (extractor) + 2b (classifier+dedup) for stronger semantic merging; this PR deliberately stays small and ships the highest-leverage deterministic fixes first.

Test plan

  • 7 new tests in tests/test_grouper.py covering header strip, Section 2 strip (EN + RU), verbatim dedup, whitespace/case normalization, same-point-different-source preserved
  • uv run pytest tests/ — 389 passed, 7 skipped, coverage 77.85%
  • uv run mypy src/ — no issues
  • uv tool run ruff check src/ tests/ — clean
  • uv run black --check src/ tests/ — clean
  • Run /digest on real channels and verify item count drop + noise quality

Files

  • src/grouper.py — strip helpers, dedup in parser
  • src/prompts/base_summary.txt — QUALITY GATE block, hard cap 5
  • src/summarizer.py — inline prompt aligned with QUALITY GATE
  • tests/test_grouper.py — 7 new test cases

belaytzev added 2 commits May 9, 2026 17:53
Reduces digest duplication and noise by addressing three sources of
low-signal output observed in production:

1. Channel summary header line (🚀 ...) was being extracted as a separate
   bullet by the grouper, duplicating Section 1 key points. Strip it
   before feeding to classification.
2. Section 2 (📎 Also/Также) low-priority items were promoted to
   cross-channel grouping where they competed with high-signal bullets.
   Strip the section before classification — Also is per-channel by design.
3. The grouper occasionally emitted the same (group, source, point)
   twice. Add deterministic dedup in _parse_grouped_response keyed on
   (group, source, normalized_point) so prompt flakes can no longer
   produce verbatim duplicates.

Also tightens the per-channel summarizer prompt with an explicit
QUALITY GATE that drops low-signal posts (photo-only, meta-empty,
expired local invites, internal admin, AI speculation) and caps
Section 1 at 5 bullets.

Measured impact (Approach A config-only baseline already cut items
80→44): these changes target the remaining 5 issue categories
identified after that baseline.
Adding deterministic dedup pushed _parse_grouped_response complexity to
12, over the project's flake8 max-complexity=10. Extract the per-group
inner loop (validation + dedup + GroupedPoint construction) into a
helper, restoring complexity under threshold.

No behavior change.
@belaytzev belaytzev merged commit fb7bbbf into main May 9, 2026
5 checks passed
@belaytzev belaytzev deleted the feat/dedup-noise-reduction branch May 9, 2026 16:06
belaytzev added a commit that referenced this pull request May 9, 2026
… chokepoint (#32)

* feat(grouper): strip noise + deterministic dedup before classification

Reduces digest duplication and noise by addressing three sources of
low-signal output observed in production:

1. Channel summary header line (🚀 ...) was being extracted as a separate
   bullet by the grouper, duplicating Section 1 key points. Strip it
   before feeding to classification.
2. Section 2 (📎 Also/Также) low-priority items were promoted to
   cross-channel grouping where they competed with high-signal bullets.
   Strip the section before classification — Also is per-channel by design.
3. The grouper occasionally emitted the same (group, source, point)
   twice. Add deterministic dedup in _parse_grouped_response keyed on
   (group, source, normalized_point) so prompt flakes can no longer
   produce verbatim duplicates.

Also tightens the per-channel summarizer prompt with an explicit
QUALITY GATE that drops low-signal posts (photo-only, meta-empty,
expired local invites, internal admin, AI speculation) and caps
Section 1 at 5 bullets.

Measured impact (Approach A config-only baseline already cut items
80→44): these changes target the remaining 5 issue categories
identified after that baseline.

* refactor(grouper): extract _collect_group_points to satisfy C901

Adding deterministic dedup pushed _parse_grouped_response complexity to
12, over the project's flake8 max-complexity=10. Extract the per-group
inner loop (validation + dedup + GroupedPoint construction) into a
helper, restoring complexity under threshold.

No behavior change.

* refactor(grouper): split into 2a extractor + 2b classifier with dedup chokepoint

Architecturally rewires the grouper from a single combined AI call (extract +
classify + dedup at once) into a two-pass pipeline with a deterministic
chokepoint between, addressing the architect's review of PR #31.

Why:
- Single combined call asks an LLM to do three jobs at once on input from
  30+ channels. At low temperature this still produces flakes: same-topic
  bullets from different channels stay separate, cross-group duplicates leak,
  and the per-channel header line gets echoed as a bullet.
- The grouper prompt was getting heavier and heavier trying to compensate.
  PR #31 added a deterministic post-dedup; this PR splits the pipeline so
  the LLM can focus on one task at a time and the deterministic step has
  more leverage.

New flow:
  Pass 2a — `_extract_all_bullets`:
    Per-channel parallel AI calls (Semaphore-bounded at concurrency 10),
    each turning one channel summary into a flat list of ExtractedBullet.
    Each extractor sees only its own channel — no cross-channel context to
    confuse classification with.

  Chokepoint — `_dedup_extracted` (only when `dedup_topics: true`):
    Cross-channel deterministic merge by normalized point text. Same story
    from N channels → 1 bullet with sources joined as comma-separated list,
    longest text variant kept. The LLM never sees the duplicates.

  Pass 2b — `_classify_bullets`:
    Single AI call over the dedup'd flat list, classifies each bullet into
    a user-defined topic group. Smaller, focused prompt — much shorter than
    the old combined prompt.

Public API unchanged: `group_summaries(channel_summaries, channel_urls)` still
returns `Dict[str, List[GroupedPoint]]`.

Also fixes core.py double-print bug:
  When a group's formatted message exceeds split_message threshold (4000
  chars), it produces multiple chunks. The old code passed every chunk's
  group name into the summary header, producing duplicates like
  "📰 News, 📰 News" in the stats line. Dedupe with `dict.fromkeys` to
  preserve order.

Tests: 33 grouper tests (all passing), full suite 400/0 with 79% coverage.
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