Три подряд доработки по плану Спринта 2.5.
1) Логи. Проблема: uvicorn ставит handlers на root-logger до того, как
отработает наш lifespan, поэтому logging.basicConfig там был no-op, и
logger.exception ничего не писал. Переносим basicConfig на уровень
импорта main.py с force=True — наш StreamHandler перебивает
uvicorn-овский root, остальные логгеры (uvicorn.access, uvicorn.error,
alembic, chromadb) остаются со своими форматами. В lifespan
basicConfig больше не зовётся.
2) Системный промпт вынесен из services/llm_client.py в
prompts/system_prompt.md. LLMClient читает файл при импорте модуля
через _load_system_prompt(); если файла нет — пустая строка + warning.
Это задел под Спринт 3, где промпт будет редактируемым и
версионируемым — физически положить его как файл дешевле, чем
держать в исходниках.
3) Markdown в ответах ассистента. Подключены marked и DOMPurify с
CDN в sandbox.html. Рендер через renderMd(text): marked.parse +
DOMPurify.sanitize — защищает от <script> на случай, если LLM вернёт
сырой HTML. Реплики пациента остаются plain text (esc). Добавлены
стили для p/ul/ol/code/pre/a/h1-h3/blockquote внутри .msg.assistant,
чтобы всё выглядело уместно в пузыре. Обёртка msg-body введена,
чтобы разделить контент и msg-meta.
План в SPRINTS.md уточнён по переиндексации — будет отдельный
endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Завершающий кусок Спринта 2 — UI для ведения диалогов.
static/sandbox.html:
- Трёхколоночная раскладка во всю высоту экрана.
- Слева: список сохранённых диалогов (имя, дата последнего обновления,
счётчик сообщений, превью первой реплики), кнопка «+ новый»;
на каждой карточке — «переименовать» (prompt) и «удалить» (confirm).
- Центр: чат в привычной стилистике (пузыри, user справа, assistant
слева), Enter — отправить, Shift+Enter — перенос строки. Заголовок
сверху показывает имя активного треда.
- Справа: отладка ответа — найденные фрагменты со score в процентах
+ собранный промпт в моноширинном блоке на светлом фоне.
- При отправке первой реплики тред создаётся автоматически, API
возвращает thread_id и thread_name — дальше реплики уходят в тот
же тред.
static/index.html: в шапке добавлены ссылки «Отладка» / «Песочница»,
подсветка активной страницы; тот же стиль nav-ссылок продублирован
в sandbox.html.
routers/chat: detail сообщения ошибки теперь включает тип исключения
(удобнее при диагностике), trace пишется через logger.exception.
SPRINTS.md: Спринт 2 помечен как закрытый.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширяем просмотр документа, чтобы оператор видел не только текстовые
чанки, но и как они лежат в ChromaDB в виде векторов — по паттерну из
work-pcs-dr-cdss.
Backend:
- services/vectorstore.get_document_chunks теперь запрашивает
include=["embeddings"] и отдаёт вектор как list[float]. Chroma
возвращает numpy-массивы, поэтому проверка наличия embeddings
сделана через len(), без or-шортката.
- models.ChunkDetail: поля embedding: list[float] + embedding_dim: int.
- routers/documents прокидывает вектор и размерность в ответ.
Frontend (static/index.html):
- В карточку чанка добавлен блок .chunk-card-actions с кнопкой
«вектор (N dim)»; раскрывается в .embedding-box с полным списком
координат (округление до 6 знаков, моноширинный шрифт, скролл).
- Функция toggleChunkText переписана через .closest + querySelector,
чтобы не ломаться от новой обёртки кнопок.
- Добавлена toggleEmb(embId).
Проверено на загруженных документах — возвращается по 1024 координаты
(E5-large), совпадает с ожиданиями embedding-модели.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>