Files
RAG_helper/main.py
T
AR 15 M4 4e45b8b181 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>
2026-04-23 10:53:01 +05:00

87 lines
2.9 KiB
Python

import asyncio
import logging
import os
import sys
from contextlib import asynccontextmanager
from alembic import command
from alembic.config import Config as AlembicConfig
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from config import settings
# Настройка логов до импорта приложения: 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__)
embedding_service: EmbeddingService | None = None
vectorstore_service: VectorStoreService | None = None
llm_client: LLMClient | None = None
def _run_migrations() -> None:
"""Автоматически подтягиваем схему до последней ревизии при старте."""
os.makedirs(os.path.dirname(settings.sqlite_path), exist_ok=True)
cfg = AlembicConfig("alembic.ini")
command.upgrade(cfg, "head")
@asynccontextmanager
async def lifespan(app: FastAPI):
global embedding_service, vectorstore_service, llm_client
logger.info("Running DB migrations…")
await asyncio.to_thread(_run_migrations)
logger.info("Loading embedding model: %s", settings.embedding_model)
embedding_service = EmbeddingService(settings.embedding_model)
logger.info("Embedding model loaded")
vectorstore_service = VectorStoreService(
persist_dir=settings.chroma_persist_dir,
embedding_service=embedding_service,
)
logger.info("ChromaDB initialized at %s", settings.chroma_persist_dir)
llm_client = LLMClient()
logger.info("LLM client ready (model=%s)", llm_client.model)
yield
logger.info("Shutting down")
app = FastAPI(
title="Chat Agent for Patients — Tuning Tool",
description="RAG-ядро и инструмент настройки пациентского чат-агента",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
from routers import chat, documents, health, query, threads # noqa: E402
app.include_router(health.router)
app.include_router(documents.router)
app.include_router(query.router)
app.include_router(chat.router)
app.include_router(threads.router)
app.mount("/", StaticFiles(directory="static", html=True), name="static")