Files
RAG_helper/main.py
T
AR 15 M4 7ec2ba3c8f feat(sprint3): редактор системного промпта и правил с версионированием
Операторы получают веб-редактор: правят системный промпт и правила,
сохраняют как новую версию, активируют, откатываются. Активная версия
используется в «Песочнице» на каждый /chat.

Принципы, согласованные заранее:
- Сохранённые версии не редактируются — только создание новой. Честный
  откат: v1 всегда та же, что была при создании.
- Правила на этом этапе — свободный markdown (textarea). Переход на
  структурированные правила (pattern → instruction) — в бэклог.
- Файл prompts/system_prompt.md становится сид-источником: при первом
  старте, если таблица agent_configs пустая, из него создаётся v1 и
  активируется. Дальше правда идёт из БД, файл не трогаем.
- rules_text конкатенируется с system_prompt в один system-message
  через compose_full_system_prompt: "{prompt}\n\nДополнительные
  правила:\n{rules}".
- Активную версию удалить нельзя — сначала активируют другую.

Модель и миграция:
- db/models/AgentConfig: id, version (unique/indexed), name (nullable),
  system_prompt, rules_text, is_active (indexed), created_at.
  Без updated_at — версии неизменяемы.
- Миграция b4450e33664d_add_agent_configs_table.

Сервисы и роутеры:
- services/config_service: ensure_seed (seed v1 из файла),
  list/get/get_active/create (version=max+1, при activate атомарно
  сбрасывает is_active у остальных и ставит новой),
  activate_config (та же схема), delete_config (возвращает причину
  отказа: not_found / active), compose_full_system_prompt.
- services/chat_service.send_message: берёт active_cfg, собирает
  system_prompt через compose_full_system_prompt, пишет
  thread.agent_config_id при создании треда (колонка была nullable
  ещё со Спринта 2 — пригодилась именно здесь).
- routers/configs: GET /configs, GET /configs/active, GET /configs/{id},
  POST /configs (activate-флаг), POST /configs/{id}/activate,
  DELETE /configs/{id} (404 / 400 если активная).
- Pydantic: AgentConfigCreateRequest, AgentConfigInfo, ListResponse,
  DeleteResponse.
- main.py: ensure_seed в lifespan после инициализации БД/Chroma/LLM.

UI:
- static/settings.html — трёхблочная страница: имя версии, textarea
  промпта, textarea правил, «Сохранить как новую» + галка
  «Сразу активировать», «Загрузить активную в редактор». Справа —
  список версий с бейджем «активная», действиями «Активировать» /
  «Удалить» (disabled у активной) / «Загрузить в редактор». При
  первом заходе активная версия автоматом подгружается в редактор.
- В nav на index.html и sandbox.html добавлена ссылка «Настройки».
- В шапке «Песочницы» — зелёный кликабельный бейдж «активная: vN · имя»
  (ведёт на /settings.html), обновляется раз в 15 с.

E2E проверено: создана v2 с правилом «ВСЕГДА начинай со слов СПАСИБО
ЗА ВОПРОС», активирована; следующий /chat вернул ответ, начинающийся
ровно с этой фразы; assembled_prompt содержит блок «Дополнительные
правила». После отката на v1 тест-v2 удалена.

SPRINTS.md: Спринт 3 помечен закрытым.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:59:06 +05:00

94 lines
3.1 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 db.session import SessionLocal # noqa: E402
from services import config_service # noqa: E402
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)
async with SessionLocal() as session:
await config_service.ensure_seed(session)
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, configs, 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.include_router(configs.router)
app.mount("/", StaticFiles(directory="static", html=True), name="static")