forked from zovos/vk_hackathon
269 lines
18 KiB
Markdown
269 lines
18 KiB
Markdown
# 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` обновляются по ходу работы, а не только в конце
|