feat(sprint2.5): логи, вынесение системного промпта, markdown-рендер

Три подряд доработки по плану Спринта 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>
This commit is contained in:
AR 15 M4
2026-04-23 10:53:01 +05:00
parent 835c0b3cc3
commit 4e45b8b181
5 changed files with 104 additions and 23 deletions
+16 -4
View File
@@ -1,6 +1,7 @@
import asyncio
import logging
import os
import sys
from contextlib import asynccontextmanager
from alembic import command
@@ -10,9 +11,21 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from config import settings
from services.embeddings import EmbeddingService
from services.llm_client import LLMClient
from services.vectorstore import VectorStoreService
# Настройка логов до импорта приложения: uvicorn ставит свои handlers
# на root-logger, поэтому basicConfig в lifespan уже не срабатывает
# (handlers есть — basicConfig no-op). force=True перебивает.
logging.basicConfig(
level=getattr(logging, settings.log_level.upper(), logging.INFO),
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
datefmt="%H:%M:%S",
handlers=[logging.StreamHandler(sys.stderr)],
force=True,
)
from services.embeddings import EmbeddingService # noqa: E402
from services.llm_client import LLMClient # noqa: E402
from services.vectorstore import VectorStoreService # noqa: E402
logger = logging.getLogger(__name__)
@@ -31,7 +44,6 @@ def _run_migrations() -> None:
@asynccontextmanager
async def lifespan(app: FastAPI):
global embedding_service, vectorstore_service, llm_client
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger.info("Running DB migrations…")
await asyncio.to_thread(_run_migrations)
logger.info("Loading embedding model: %s", settings.embedding_model)