Phase 0 — Foundation

Digest System · feed-gathering
Детерминированный пайплайн от сырых сообщений до готовых чанков.
Без LLM, без API ключей. Всё тестируемо локально.
Нажми на карточку чтобы раскрыть детали
1

0.1 Dependencies

Добавить openai SDK для CLIProxy совместимости

Что сделать:

  • Добавить openai>=1.30 в requirements.in
  • Запустить pip-compile requirements.in для обновления lock-файла
  • Проверить что pip install -r requirements.txt ставит без конфликтов

Зачем openai, а не anthropic:

  • Все LLM-вызовы идут через CLIProxy (http://127.0.0.1:31337/v1)
  • CLIProxy говорит на OpenAI-совместимом протоколе
  • Anthropic-специфичные фичи (prompt caching, tool-use) проксируются через extra_headers
requirements.inrequirements.txt
2

0.2 Config

Конфигурационные ключи для digest system

Новые переменные в app/config.py:

  • DIGEST_LLM_BASE_URL = http://127.0.0.1:31337/v1 — endpoint CLIProxy
  • DIGEST_PROMPT_VERSION = "v1" — версия промптов, для A/B тестов
  • DIGEST_CLASSIFY_MODEL = anthropic/claude-haiku-3.5 — дешёвая модель для Pass 1
  • DIGEST_MAP_MODEL = anthropic/claude-sonnet-4-6 — основная модель для map step
  • DIGEST_REDUCE_MODEL = anthropic/claude-sonnet-4-6 — модель для reduce step
  • DIGEST_MAX_CHUNKS = 10 — максимум чанков на одно окно
  • DIGEST_CHUNK_TOKEN_BUDGET = 7000 — целевой бюджет токенов на чанк

Принцип:

  • Все значения через os.getenv() с дефолтами — без env переменных работает из коробки
  • API ключ не нужен — CLIProxy сам роутит к провайдерам
  • Нет MIN_MESSAGES порога — даже одно сообщение может нести ценность (tips-and-tricks каналы). Фильтрация шума — задача pre-processing и классификатора
app/config.py
3

0.3 DB коллекции + индексы

4 новых коллекции в MongoDB gathering

Коллекции:

  • discord_digest — готовые дайджесты. Основные индексы: (server_id, window_end_at desc), (channel_id, window_end_at desc), unique (extraction_window_id, prompt_version), text index на overview + search_keywords
  • discord_digest_job — очередь задач. TTL 7 дней. Индексы: (status, created_at), (channel_id, created_at desc)
  • digest_topic_profiles — профили тем (AI image gen, etc.). _id = строковый slug
  • digest_entity_dictionary — канонические сущности (Flux, SDXL, ComfyUI). Индекс по profile_id

Паттерн:

  • Добавить accessor-функции в app/db.py (как существующие discord_server_col(), discord_message_col())
  • Индексы через _ensure_indexes() — идемпотентные, создаются при старте
app/db.py
4

0.4 Pydantic модели

Типизированные модели данных для всего digest pipeline

Основные модели:

  • ContentPointer{message_id, char_start, char_end}. Ядро pointer extraction
  • SignalQuality{level: empty|low|medium|high, reason: str}
  • KnowledgeItem{kind, title, content_pointer?, content?, context_version?, entity_refs, source_url?, attachment_ref?, message_ids, supersedes?, conflicts_with?}
  • Decision, ActionItem, OpenQuestion — без attribution полей (автор берётся из message_ids → исходное сообщение в базе)
  • ChunkSummary — выход map step (topics, decisions, action_items, knowledge_items, etc.)
  • Digest — финальный выход reduce step (всё из ChunkSummary + title, overview, highlights, coverage)
  • DigestJob — задача в очереди (scope, trigger, status, window, token_usage, cost_estimate)
  • TopicProfile — профиль темы с composable модулями
  • ProfileModule — отдельный модуль (item kinds, prompt supplement, feature flags)
  • EntityEntry — сущность в словаре (canonical_name, aliases, entity_type, version_tracked)
  • ThreadClassification — выход Pass 1 (interesting, primary_kinds, profile_routing)

Принципы:

  • content_pointer и content взаимоисключающие (pointer для verbatim, content для synthesized)
  • Нет отдельных полей авторства — автор вычисляется из message_ids при отображении
  • Валидация через Pydantic validators — сразу ловит невалидные данные
app/digest/models.py
5

0.5 Pre-processing

Фильтрация, нормализация, feature flags — всё до LLM

Фильтрация (drop):

  • System messages (type != 0): join/leave/pin/boost
  • Пустые: нет content, attachments, embeds, referenced_message
  • Emoji-only: один эмодзи или кастомный эмодзи, больше ничего
  • Боты: по configurable allowlist (по умолчанию — дропать всех)

Нормализация:

  • <@123>@display_name + сохранить маппинг user_id → name
  • <#123>#channel-name
  • Кастомные эмодзи → :name:
  • Аттачменты → метаданные ([image: file.png 1920x1080]), машиночитаемые файлы (JSON, YAML) — парсить детерминированно
  • Ссылки — сохранять как есть
  • Код-блоки — НЕ трогать (whitespace важен для pointer extraction)

Feature flags (per message):

  • has_link, has_attachment, has_embed, has_code_block, has_workflow_asset
  • is_reply, reaction_count=N, attachment_parsed=true
  • explicit_decision_phrase, explicit_action_phrase, explicit_question_phrase
  • error_or_failure_terms, deadline_or_time_commitment

Вход/выход:

  • Вход: список сырых MongoDB документов из discord_message
  • Выход: список нормализованных сообщений с flags + author map (user_id → display_name)
app/digest/preprocess.py
6

0.6 Thread reconstruction

Группировка сообщений в треды: reply graph + эвристики

Layer 1 — Explicit (детерминированный):

  • Построить reply graph из referencedMessage.id
  • Найти root сообщения (нет родителя или родитель за пределами окна)
  • Сгруппировать root + все transitive потомки → тред
  • Объединить треды со shared mention targets

Layer 2 — Implicit (эвристика):

  • Temporal proximity: сообщения в пределах 120 секунд от тех же участников → вероятно один разговор
  • Lexical overlap: общие технические термины (необычные существительные) рядом по времени
  • Shared context: ссылки на один и тот же аттачмент или URL

Orphan handling:

  • Сообщения без связей после Layer 1+2 → single-message треды
  • Это нормально — LLM может извлечь из standalone сообщений с ссылками или кодом

Выход:

  • Список тредов, каждый — отсортированный по timestamp список сообщений
  • Треды отсортированы по времени первого сообщения
Reply graph покрывает ~20-40% реальной структуры. Layer 2 поднимает покрытие. Цель — не идеальный disentanglement, а уменьшение ложных orphan'ов.
app/digest/threads.py
7

0.7 Importance scoring

Взвешенный скоринг для приоритизации и coverage check

Сигналы и веса (дефолтные, настраиваемые):

  • sum(reactions.count) → 2.0 ⚠️ мемы скорят высоко
  • distinct(reactions.emoji) → 1.0
  • Глубина треда (transitive replies) → 3.0 ⚠️ flamewars тоже глубокие
  • Уникальные участники в треде → 2.0
  • Есть URL → 1.5
  • Есть embed → 1.0
  • Есть code block → 2.0
  • Есть attachment → 1.0
  • Есть workflow asset → 2.0
  • @everyone / @here / role mention → 2.5
  • Длина 80-600 символов → 1.0

Важно:

  • Веса — некалиброванные дефолты. Калибровать на gold set после Phase 1
  • Реакции: учитывать тип (📌✅🔥 = информационные, 😂💀 = социальные)
  • Score используется ТОЛЬКО для ранжирования, НИКОГДА для фильтрации

Выход:

  • Каждое сообщение получает importance_score: float
  • Функция get_top_messages(n) возвращает top-N по скору
app/digest/scoring.py
8

0.8 Chunking

Thread-first chunking с XML форматом сообщений

Алгоритм:

  • Идти по тредам в хронологическом порядке
  • Добавлять тред в текущий чанк, пока не превышен бюджет (~7000 токенов)
  • Никогда не разрезать тред между чанками (кроме случая когда тред сам больше бюджета)
  • Если тред больше бюджета → резать по границам turns с overlap 2 сообщения

Turn collapse:

  • Сообщения от одного автора в пределах 120 секунд → один turn block
  • Индивидуальные message ID сохраняются (нужны для citation)

Content truncation:

  • Per-message лимит 1000 символов с маркером
  • Pointer extraction ссылается на ПОЛНЫЙ нормализованный текст, не на truncated

XML формат сообщения:

  • <message id="snowflake" ts="ISO" author="name" user_id="uid">
  • features: has_link, has_code_block, reaction_count=4
  • text: ...
  • reply_to: snowflake | none
  • </message>

Токен эстимация:

  • len(normalized_text) / 3.5 — достаточно для решений по чанкингу
app/digest/chunking.py
9

0.9 Entity dictionary

CRUD + seed каноническими сущностями для AI image gen

Модель EntityEntry:

  • _id: str — канонический ID, напр. "flux-1-dev"
  • canonical_name: str"Flux.1-dev"
  • entity_type: str"model" | "tool" | "technique" | "parameter"
  • aliases: list[str]["Flux", "flux-dev", "FluxDev"]
  • version_tracked: bool — разные версии = разные сущности?
  • profile_id: str — к какому профилю привязана

Seed данные (ai-image-generation):

  • Модели: flux-1-dev, sdxl, sd-1-5, sd-3, hidream
  • Инструменты: comfyui, automatic1111, forge, kohya-ss, ai-toolkit
  • Техники: lora, dreambooth, textual-inversion, controlnet, ipadapter
  • Параметры: cfg-scale, sampler, scheduler, steps

Функции:

  • resolve_entity(text) → canonical_id | None — поиск по алиасам
  • get_entities_for_profile(profile_id) → list
  • CRUD для GraphQL
app/digest/entities.py
10

0.10 Topic profiles

Composable модульные профили для доменной экстракции

Структура профиля:

  • _id: str — slug, напр. "ai-image-generation"
  • name: str — человекочитаемое название
  • modules: list[str] — ID подключённых модулей
  • prompt_supplement: str — собранный из модулей суппlement для system prompt

Composable модули:

  • base-chat — generic decisions, questions, links (всегда включён)
  • creative-prompts — detection + pointer extraction промптов генерации
  • training-configs — конфиги обучения, датасеты, гиперпараметры
  • comfyui-workflows — JSON workflow файлы, node graphs
  • troubleshooting — пары ошибка → решение

Гранулярность:

  • Per-channel (discord_channel.digest_profile_id) — приоритет
  • Per-server (discord_server.digest_profile_id) — fallback

Эволюция (Phase 2+):

  • LLM может создавать candidate_kind items — новые типы, не вписывающиеся в схему
  • Периодически кластеризовать candidate_kind → предлагать новые типы оператору
  • Профили — гипотезы, не законы
app/digest/profiles.py
11

0.11 Тесты

Unit tests на реальных данных из MongoDB

Тест-файлы:

  • test_digest_preprocess.py — фильтрация (system messages дропаются, нормальные остаются), нормализация (mentions → names, attachments → metadata), feature flags корректны
  • test_digest_threads.py — reply graph строит правильные группы, Layer 2 подбирает implicit связи, orphan'ы корректно изолированы
  • test_digest_chunking.py — треды не разрезаются, бюджет соблюдается, XML формат валиден, turn collapse работает

Тестовые данные:

  • Snapshot реальных сообщений из MongoDB (экспорт 50-100 сообщений из активного канала)
  • Fixtures в tests/fixtures/digest/
  • Edge cases: пустое окно, один гигантский тред, все orphan'ы, code-heavy канал
tests/test_digest_preprocess.py tests/test_digest_threads.py tests/test_digest_chunking.py tests/fixtures/digest/
Инфраструктура
Модели данных
Пайплайн
Домен
Тесты

✅ Exit Criteria

Взять реальный канал, прогнать pipeline до чанков, получить well-formed XML блоки готовые для LLM.
Entity dictionary заполнен. Все Pydantic модели определены. Тестируемо без API ключей.

← Назад к общему плану · GitHub Issue #3