7ec2ba3c8f
Операторы получают веб-редактор: правят системный промпт и правила,
сохраняют как новую версию, активируют, откатываются. Активная версия
используется в «Песочнице» на каждый /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>
94 lines
3.1 KiB
Python
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")
|