# 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` обновляются по ходу работы, а не только в конце