vk_hackathon/doc/prompt.md

269 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Prompt For Next Refactor Pass
Считай этот файл каноническим планом работ по репозиторию. Старые заметки в `doc/todo.md`, `doc/todo_and_pipeline.md`, `doc/todo_people.md` и `doc/ai_update.md` можно использовать как справку, но не как основной источник правды.
## Контекст
В репозитории два сервиса:
- `index` строит чанки для индексации
- `search` получает вопрос и возвращает `message_ids`
Контракты `POST /index`, `POST /sparse_embedding` и `POST /search` менять нельзя.
Дополнительный контекст по текущему состоянию:
- worktree уже грязный, не откатывай чужие правки
- `main` отстает от `origin/main` на 3 коммита
- `.ai_update/` должен быть shared-state каталогом, но сейчас он игнорируется через `.gitignore`
- реальная логика почти целиком живет в `index/main.py` и `search/main.py`, поэтому следующий шаг должен быть не только про качество поиска, но и про разбиение кода на понятные модули
## Что уже очевидно сломано или недоделано
### P0. Исправить критические дефекты в `search`
- В `search/main.py` есть реальный баг: `must_conditions: []` не создает список. При запросах с `date_range` или `asker` код упадет на `.append()`. Исправить первым коммитом.
- Поиск использует только `question.text`, хотя схема уже содержит `search_text`, `variants`, `hyde`, `keywords`, `entities`, `date_mentions`, `date_range`.
- После rerank теряется хвост retrieval-кандидатов.
- Финальный список `message_ids` не дедуплицируется, не агрегируется по score и не ограничивается `top-50`, хотя метрика считается именно на `K=50`.
- Внешние HTTP-вызовы dense/rerank не имеют нормальных `timeout` и `retry`.
### P1. Перестроить индексацию под структуру чата
- Сейчас `index` режет текст по символам, а не по сообщениям.
- Overlap строится по хвосту строки, а не по границам сообщений.
- `page_content`, `dense_content` и `sparse_content` сейчас одинаковые, хотя должны выполнять разные задачи.
- В индекс почти не попадают важные сигналы: `sender_id`, `mentions`, `file_snippets`, `member_event`, `thread_sn`, маркеры `quote` и `forward`.
### P2. Начать использовать metadata осмысленно
- В README прямо указаны `participants`, `mentions`, `contains_forward`, `contains_quote`.
- В `search/main.py` есть модель `ChunkMetadata`, но retrieval почти не использует metadata для фильтрации и буста.
- Нужно поддержать фильтры/бусты по людям, mentions, thread, дате, quote/forward и не ломать контракт ответа.
### P3. Привести инфраструктуру и документацию в порядок
- `docker-compose.yml`, `README.md`, `Makefile` и `doc/upload_to_docker.md` частично расходятся по сценарию запуска и сборки.
- В `Makefile` нет `--platform linux/amd64`, хотя в документации на загрузку образов это требуется.
- В репозитории нет нормального `bin/codex-start`, хотя workflow на него ссылается.
- Планирование размазано по нескольким файлам вместо одного документа.
## Что нужно сделать
### 1. Рефакторинг `search`
Сначала разбей `search/main.py` на несколько логических частей. Минимально:
- `search/config.py`: env, валидация конфигурации, auth-настройки
- `search/schemas.py`: pydantic-модели запросов и ответов
- `search/query_builder.py`: сборка dense/sparse запросов из `question`
- `search/retrieval.py`: `Qdrant` prefetch, filters, fusion
- `search/rerank.py`: вызов reranker и работа с rerank-кандидатами
- `search/aggregation.py`: дедуп message ids, score aggregation, top-50
- `search/main.py`: только wiring FastAPI и вызовы сервисных функций
Что должно измениться по логике:
- основной dense query: `question.search_text.strip()` с fallback на `question.text.strip()`
- дополнительные dense query: `question.variants`, `question.hyde`
- основной sparse query: `keywords`, а если их нет, то нормализованный базовый запрос
- entity-сигналы: `people`, `emails`, `documents`, `names`, `links` использовать как lexical boost или metadata filter
- `date_range` и, по возможности, `date_mentions` использовать для фильтрации по `metadata.start` / `metadata.end`
- retrieval должен возвращать расширенный пул кандидатов
- rerank должен сортировать top-N, но не уничтожать полностью хвост retrieval
- финальный ответ должен:
- агрегировать score по `message_id`
- удалять дубликаты
- ограничиваться `top-50`
Отдельно:
- убери импорт-тайм побочный эффект `validate_required_env()` и переведи его в более тестируемую точку старта
- добавь явные `timeout` для `httpx.AsyncClient`
- добавь retry-политику на ошибки сети и 5xx
### 2. Рефакторинг `index`
Разбей `index/main.py` хотя бы так:
- `index/schemas.py`: request/response модели
- `index/rendering.py`: извлечение и разметка текста сообщения
- `index/cleaning.py`: локальная очистка и нормализация raw message payload
- `index/chunking.py`: сборка окон сообщений и overlap по сообщениям
- `index/sparse.py`: локальная sparse-эмбеддинг логика
- `index/main.py`: только FastAPI wiring
Что должно измениться по логике индексации:
- базовая единица чанка: сообщение, а не кусок строки
- окно чанка должно учитывать:
- число сообщений
- суммарную длину
- time gap между сообщениями
- границы thread/forward/quote, если они явно ломают контекст
- overlap должен повторять последние сообщения, а не последние символы
- `render_message()` должен материализовать:
- автора сообщения
- mentions
- quote / forward маркеры
- `file_snippets`
- `member_event`
- при необходимости `thread_sn`
### 2.1. Локальная очистка сообщений по реальному формату `data/Go Nova.json`
Очистка должна происходить локально внутри `index`, без внешних API и без попытки делегировать нормализацию в dense/rerank сервисы.
Что показал реальный датасет:
- значимая часть сообщений имеет пустой верхнеуровневый `text`
- смысл часто лежит в `parts[*].text`
- в `parts[*]` используется поле `mediaType`, а не `type`
- встречаются `mediaType = text`, `quote`, `forward`
- есть `member_event` без обычного текста
- `file_snippets` приходит JSON-строкой
- в данных встречаются URL, email и zero-width символы
Минимальный pipeline очистки:
1. Извлечение raw сигналов:
- `text`
- `parts`
- `mentions`
- `member_event`
- `file_snippets`
- `sender_id`
- флаги `is_system`, `is_forward`, `is_quote`
2. Unicode и whitespace normalization:
- удалить `\u200b`, `\u200c`, `\u200d`, `\ufeff`
- унифицировать переводы строк
- схлопнуть лишние пробелы и пустые строки
- не удалять email, URL, версии, имена файлов и технические токены
3. Нормализация `parts`:
- `mediaType = text`: включать как основной контент
- `mediaType = quote`: явно материализовать как `quote_from` + `quote_text`
- `mediaType = forward`: явно материализовать как `forwarded_from` + `forward_text`
- неизвестные типы не выбрасывать, а сохранять как маркированные текстовые блоки
4. Нормализация системных событий:
- `member_event` превращать в индексируемый текст
- минимум поддержать `addMembers`
- для неизвестных event type сохранять тип и payload в безопасной текстовой форме
5. Нормализация файлов:
- распарсить `file_snippets` локально из JSON-строки
- вытащить `name`, `mime`, `original_url`, `date_create`
- при невалидном JSON не падать, а сохранять `attachment_raw`
6. Правило пропуска:
- выбрасывать сообщение только если после очистки пусты и `text`, и `parts`, и `member_event`, и `file_snippets`
Развести три представления текста:
- `page_content`: читаемый текст чанка для payload
- `dense_content`: нормализованный текст с role-маркерами, авторами и служебным контекстом
- `sparse_content`: keyword-heavy текст, куда попадают имена людей, mentions, email, документы, файлы, ссылки, важные термины
При этом:
- не меняй внешний контракт `POST /index`
- сохрани понятную привязку `message_ids` к каждому чанку
- делай chunking объяснимым, а не магическим
### 3. Улучшить metadata-aware retrieval
После стабилизации `search` и `index`:
- добавь boost/filter по `participants`
- добавь boost/filter по `mentions`
- используй `contains_quote` и `contains_forward` как вторичные сигналы ранжирования
- если в payload есть `thread_sn`, учитывай его для вопросов про конкретную ветку обсуждения
- подбери новые значения для `DENSE_PREFETCH_K`, `SPRASE_PREFETCH_K`, `RETRIEVE_K`, `RERANK_LIMIT`
Если multi-query fusion в `Qdrant` начинает заметно улучшать recall, оставляй его. Если только усложняет код без эффекта, не тащи лишнюю сложность.
### 4. Навести порядок в repo hygiene
- перестань держать `.ai_update/` в `.gitignore`, если workflow действительно предполагает коммит этого каталога
- либо добавь реальный `bin/codex-start`, либо убери ссылки на него из документации
- приведи `docker-compose.yml` к тому же сценарию env, что и `README.md`
- добавь `--platform linux/amd64` в команды сборки из `Makefile`
- оставь `doc/prompt.md` основным планом, а дублирующие `todo`-файлы сократи или архивируй
### 5. Логирование каждого изменения и отчетность
Во время следующей реализации нельзя ограничиваться только кодом. После каждого meaningful change нужно фиксировать, что именно сделано и что сохранено.
Обязательные действия:
- после каждого существенного изменения обновлять `.ai_update/changelog.md`
- поддерживать `.ai_update/touched_files.md`
- обновлять `.ai_update/current_status.md` и `.ai_update/handoff.md` к концу сессии
- вести [doc/output.md](/home/q/doc/hackaton/doc/output.md) как человекочитаемый отчет по ходу работ
Что писать в `doc/output.md` после каждой существенной правки:
- дата/время
- что изменено
- какие файлы изменены
- зачем это сделано
- как это проверено
- что осталось недоделанным или рискованным
## Какие тесты и проверки нужны
Создай минимальный тестовый контур. Без этого рефакторинг превратится в угадывание.
### Unit tests
- `index/cleaning.py`: unicode/whitespace cleanup, `mediaType`, `member_event`, `file_snippets`
- `index/rendering.py`: сообщение с `parts`, `quote`, `forward`, `mentions`, `file_snippets`, `member_event`
- `index/chunking.py`: chunking по сообщениям, time gap, overlap по сообщениям
- `search/query_builder.py`: `search_text`, fallback на `text`, `variants`, `hyde`, `keywords`, entities, date range
- `search/aggregation.py`: dedup, score aggregation, `top-50`
### Smoke checks
- `python3 -m py_compile index/main.py search/main.py`
- `docker compose config`
- локальный запуск через `docker compose up --build`, если заполнен `.env`
- ручной smoke `curl` на `/health`, `/index`, `/search`
### Regression set
Зафиксируй отдельный markdown-файл с контрольными вопросами. Включи хотя бы такие классы запросов:
- кто что писал
- кого упоминали
- что писали про документ, файл или ссылку
- что обсуждали в конкретный период
- что было в пересланных сообщениях и цитатах
- что было в системных событиях и прикреплениях
Используй `data/Go Nova.json` как локальную fixture-основу.
## Порядок внедрения
1. Сначала внедрить и протестировать локальную очистку сообщений в `index/cleaning.py` на кейсах из `data/Go Nova.json`.
2. Переделать `index` на message-based chunking и разные `page_content` / `dense_content` / `sparse_content`.
3. После стабилизации входного текста починить P0 баги в `search` и добавить тесты на query builder и aggregation.
4. Вынести `search` из монолита `main.py` в модули без изменения API.
5. Подключить metadata-aware retrieval и тюнинг параметров.
6. Синхронизировать docker/docs/workflow и убрать repo hygiene противоречия.
## Критерий готовности
Можно считать работу завершенной только если одновременно выполнено все ниже:
- `search` использует не только `question.text`
- поиск не падает на `date_range` и `asker`
- retrieval + rerank не теряют кандидатов бессмысленно
- финальная выдача дедуплицирована и ограничена `top-50`
- `index` режет по сообщениям, а не по символам как основной механизм
- локальная очистка сообщений работает без внешних API и покрывает `parts`, `member_event`, `file_snippets`, URL и zero-width артефакты
- `page_content`, `dense_content`, `sparse_content` различаются по назначению
- в индекс и retrieval реально включены metadata и скрытые сигналы чата
- локальная документация, compose и сборка образов не противоречат друг другу
- история изменений и отчет в `doc/output.md` обновляются по ходу работы, а не только в конце