3c2657ab99
Второй кусок Спринта 2: агент теперь помнит контекст. RAG-retrieval
делается по последней реплике пациента, в LLM уходит системный промпт +
последние 20 сообщений треда + новая реплика + найденные фрагменты.
Backend:
- services/chat_service: send_message — создаёт тред при необходимости
(auto-имя из первой реплики + UTC-дата), сохраняет user-реплику до
вызова LLM (чтобы не потерять при сбое), делает retrieval, грузит
историю треда (desc/limit 20 → reversed для хронологии), зовёт
llm.chat, сохраняет ответ ассистента вместе с sources_json и
assembled_prompt, обновляет thread.updated_at. Плюс list_threads с
JOIN-выборкой превью первой реплики и счётчика сообщений,
get_thread_detail через selectinload, rename_thread, delete_thread
(CASCADE на FK делает уборку сообщений автоматически, но
explicit delete оставлен для подсчёта удалённых).
- services/llm_client.chat: принимает history=[{role, content}, ...],
собирает messages = [system, ...history, user-с-RAG]; assembled_prompt
дампит всю цепочку в виде [SYSTEM]/[USER]/[ASSISTANT]-блоков для
отображения в Debug UI.
- routers/chat: POST /chat, обрабатывает LookupError → 404.
- routers/threads: GET /threads, GET /threads/{id}, PATCH /threads/{id}
(переименовать), DELETE /threads/{id}.
- models: ChatRequest, ThreadRenameRequest; ChatResponse, ThreadInfo,
ThreadListResponse, ThreadDetailResponse, MessageInfo,
ThreadDeleteResponse.
Запуск:
- В lifespan main.py: автоматический alembic upgrade head через
asyncio.to_thread (сам alembic делает asyncio.run внутри, его нельзя
звать из уже работающего event loop). LLMClient инициализируется
один раз при старте — вместо создания на каждый запрос.
E2E проверено: новый тред → агент отвечает и просит представиться;
вторая реплика в том же треде — агент помнит контекст; PATCH
переименовывает; DELETE удаляет тред с каскадом на сообщения.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.4 KiB
Python
75 lines
2.4 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
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
|
|
from services.embeddings import EmbeddingService
|
|
from services.llm_client import LLMClient
|
|
from services.vectorstore import VectorStoreService
|
|
|
|
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
|
|
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)
|
|
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")
|