feat(sprint8.5+8.6): чанкер v2 (иерархия H1/H2/H3) + регрессия 4 веток в UI
Sprint 8.5 — чанкер v2 (services/document_processor.py):
- markdown-it-py для md-входа: каждый H2 открывает свою секцию, H3 идёт в тело
- множественные H1 — штатный кейс (new_booking.md = 8 H1, шаги воронки + группы);
H1 без H2 → секция heading=H1; преамбула H1 (тело до первого H2) игнорируется
- YAML frontmatter (--- ... ---) отрезается, в индекс не попадает
- breadcrumb «## {H2}» как первая строка каждого subchunk'а
- merge коротких хвостов и sentence-overlap — только внутри одной H2-секции
- excluded_section_headings в config.py
- 17 unit-тестов на stdlib unittest (tests/test_document_processor_v2.py),
включая smoke по реальным general_info.md (тимпанометрия → правильная секция)
и new_booking.md (защита от регрессии множественных H1)
- ТЗ: docs/CHUNKER_v2_TZ.md
Sprint 8.6 — регрессия остальных 4 веток (static/regression.html):
- 4 опции в селекторе режима: branch:price_question (40 кейсов),
branch:medical_question (29), branch:escalate_human (14), branch:reschedule (16)
- бэкенд из 8b уже параметрический — правок в сервисе не потребовалось
- new_booking вне скоупа — state-machine, под него отдельный 8c (multi-turn)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -663,6 +663,85 @@
|
||||
|
||||
---
|
||||
|
||||
## Спринт 8.5. Чанкер v2 (markdown с иерархией H1/H2/H3)
|
||||
|
||||
### Цель
|
||||
Сделать нарезку wiki-датасетов предсказуемой и совместимой с eval-контрактом «`metadata.section` чанка == заголовок H2 раздела». Триггер — фейл регрессии 8b по тимпанометрии: `expected_doc_section: "Направления приёма"` не находится из-за того, что текущий парсер режет markdown эвристиками без учёта иерархии, склеивает соседей через границы H2 и подмешивает overlap чужой секции. Полное ТЗ — `docs/CHUNKER_v2_TZ.md`.
|
||||
|
||||
### Статус: ✅ Закрыт
|
||||
|
||||
### Задачи
|
||||
|
||||
**Парсинг (`services/document_processor.py::parse_markdown`, ветка `is_markdown=True`):**
|
||||
- [x] Перейти на `markdown-it-py` (уже в зависимостях транзитивно), регэкспные эвристики `numbered_heading_re` / `faq_question_re` / ALL-CAPS — отключить для md-входа (для txt оставить как есть).
|
||||
- [x] Каждый H2 открывает свою секцию; H3 и ниже идут в тело текущей H2 как строка `### {текст}`.
|
||||
- [x] Множественные H1 — штатный кейс (`new_booking.md` имеет 8 H1 — шаги воронки + группы). Каждый H1 группирует свои H2-секции; преамбула H1 (тело до первого H2) игнорируется. Если внутри H1 нет ни одного H2 — H1 сам становится одной секцией с heading=H1. Служебные блоки операторы держат в отдельном файле `docs/wiki_meta_<branch>.md` (вне `data/datasets/`), парсеру их различать не нужно.
|
||||
- [x] YAML frontmatter (`--- ... ---` в самом начале файла) распарсить, вернуть отдельным полем `document_metadata`, в текст не пропускать.
|
||||
|
||||
**Чанкинг (`services/document_processor.py::chunk_sections`):**
|
||||
- [x] Резка длинных H2 по абзацам (`\n\n`, не `\n`).
|
||||
- [x] В каждый subchunk добавлять breadcrumb-префикс `## {heading H2}` как первую строку. `section` во всех subchunk'ах одинаков.
|
||||
- [x] Merge коротких хвостов — только внутри одной H2-секции. Через границу H2 склеивать запрещено.
|
||||
- [x] Sentence-overlap — только между subchunk'ами одной H2. Между разными секциями overlap'а нет.
|
||||
|
||||
**Конфиг (`config.py`):**
|
||||
- [x] `excluded_section_headings: list[str] = []` — H2 из этого списка не индексируются (под будущую внешнюю вики).
|
||||
|
||||
**Тесты (новый каталог `tests/`, на stdlib `unittest` — без новых зависимостей):**
|
||||
- [x] `tests/test_document_processor_v2.py`. Запуск: `.venv/bin/python -m unittest tests.test_document_processor_v2 -v` (17 кейсов, все зелёные).
|
||||
- `general_info.md` → все `section` непустые, нет ни одного `section`, начинающегося с цифры; чанк, содержащий «тимпанометр», имеет `section == "Направления приёма"`; в каждом чанке первая строка — breadcrumb `## {section}`.
|
||||
- `new_booking.md` (8 H1) → секции из всех H1-групп индексируются; точечно проверяем «Тон и формулировки», «Шаблон ответа (5 пунктов)», «Текст-завершение».
|
||||
- Файл с frontmatter → frontmatter не утекает в чанки; первый чанк начинается с `## {первый H2}`.
|
||||
- Множественные H1 с H2 → секции из всех H1; преамбула H1 (тело до первого H2) выкидывается; WARN на втором H1 не возникает (старое правило отозвано).
|
||||
- H1 без H2 → одна секция с heading=H1.
|
||||
- H3 внутри H2 → один чанк с `section == H2`, в теле строка `### {H3}`.
|
||||
- Длинная H2-секция → N subchunk'ов, у всех одинаковый `section`, у каждого первая строка `## {H2}`.
|
||||
- Нумерованный список «1. … 2. …» в md-входе → не парсится как заголовок.
|
||||
|
||||
### Что не делаем
|
||||
- Не трогаем embeddings, reranker, гибридный retrieval, HyDE — отдельные спринты.
|
||||
- `parse_pdf` / `parse_docx` не трогаем; если в них всплывут аналогичные проблемы — отдельной итерацией.
|
||||
- Формат хранения в Chroma не меняем — `metadata.section` остаётся строкой.
|
||||
|
||||
### Критерий готовности
|
||||
- [x] `python -m unittest tests.test_document_processor_v2 -v` — 17 кейсов, все зелёные.
|
||||
- [x] Smoke-прогон чанкера на `data/datasets/general_info.md`: чанк с «тимпанометр» имеет `section == "Направления приёма"`, нет чанков с пустым `section`, нет чанков с `section`, начинающимся с цифры.
|
||||
- [x] После переиндексации документа через UI «Отладка» прогон регрессии `branch:general_info` показал PASS на тимпанометрии и других ранее падавших кейсах (подтверждено пользователем 2026-05-04).
|
||||
|
||||
---
|
||||
|
||||
## Спринт 8.6. Регрессия остальных веток (price_question, medical_question, escalate_human, reschedule)
|
||||
|
||||
### Цель
|
||||
Расширить регрессию ответов веток (механика 8b) на все остальные ветки, кроме `new_booking`. Бэкенд из 8b уже универсальный — читает `eval/branch_cases_<intent_code>.jsonl` по имени, никаких правок в сервисе. Минимальная работа — добавить опции в селектор режима на странице «Регрессия».
|
||||
|
||||
`new_booking` намеренно оставлен вне скоупа: это state-machine-ветка с многошаговой воронкой, single-turn регрессия неправильно покажет результат — отдельная задача в Спринте 8c (multi-turn).
|
||||
|
||||
### Статус: ✅ Закрыт
|
||||
|
||||
### Задачи
|
||||
|
||||
**UI (`static/regression.html`):**
|
||||
- [x] В select `id="mode-select"` добавлены 4 опции:
|
||||
- `<option value="branch:price_question">Ветка · price_question</option>` (40 кейсов)
|
||||
- `<option value="branch:medical_question">Ветка · medical_question</option>` (29 кейсов)
|
||||
- `<option value="branch:escalate_human">Ветка · escalate_human</option>` (14 кейсов)
|
||||
- `<option value="branch:reschedule">Ветка · reschedule</option>` (16 кейсов)
|
||||
- [x] `setMode` / `currentBranchIntent()` параметричны (`mode.split(":", 2)[1]`) — правок не потребовалось.
|
||||
|
||||
**База кейсов (уже в репо):**
|
||||
- [x] `eval/branch_cases_price_question.jsonl`
|
||||
- [x] `eval/branch_cases_medical_question.jsonl`
|
||||
- [x] `eval/branch_cases_escalate_human.jsonl`
|
||||
- [x] `eval/branch_cases_reschedule.jsonl`
|
||||
|
||||
### Критерий готовности
|
||||
- [x] На странице «Регрессия» в селекторе режима видны 5 опций веток (general_info + 4 новые).
|
||||
- [x] Smoke-прогон через UI для каждой из 4 новых веток — осмысленный ответ + retrieved-секции (подтверждено пользователем 2026-05-04).
|
||||
- [x] При активации новой версии промпта ветки кэш для неё пуст — поведение 8b сохраняется параметрически.
|
||||
|
||||
---
|
||||
|
||||
## Спринт 9. Сценарии + экспорт графа
|
||||
|
||||
### Цель
|
||||
|
||||
Reference in New Issue
Block a user