Files
RAG_helper/main.py
T
AR 15 M4 9eef2dab3a feat(sprint6a): блок A — structured output, intent_steps, sticky-удержание
Заменили строковый тег [STATE: ...] из Спринта 5 на структурированный выход
ветки в виде JSON-блока в хвосте ответа: {state_after, slots_updated}, парсимый
балансировкой скобок. Шаги state machine вынесены из монолитного промпта в
таблицу intent_steps (intent_id FK, code, name, order_index, system_prompt,
allowed_next JSON, guards JSON) и редактируются через UI. Валидатор переходов
сверяет state_after с allowed_next и блокирует невалидные прыжки.

Базовый промпт new_booking разбит на base + 6 файлов шагов (intro/qualify/
present/offer_time/book/close), которые сидятся при старте через
ensure_seed_steps. В chat_service промпт собирается как base + step + блок
[ТЕКУЩЕЕ СОСТОЯНИЕ].

Попутно реализован мини-блок G (sticky state machine): когда диалог идёт по
sm-ветке и роутер на новой реплике предлагает другую — state НЕ сбрасывается,
в системный промпт ветки подаётся блок [ПОДСКАЗКА РОУТЕРА], LLM сама решает
(STATE_JSON или INTENT_CHANGE). Это сняло ключевую дыру Спринта 5: «Меня
зовут Алексей» / «болит ухо» внутри записи больше не сбрасывают сценарий.

Промпт ветки new_booking ужесточён: бытовые жалобы — это повод записи (слот
reason + сочувствие), не повод уводить в medical_question. Шаг present теперь
использует reason в формулировке. Промпт _router расширен живыми примерами
для всех 6 веток, особенно для reschedule («не смогу подойти», «перенесите»).

Надёжность внешнего LLM:
- ретрай в LLMClient с паузой 500 мс + новое исключение LLMUnavailableError;
- ретрай в RouterClient (DeepSeek периодически моргает);
- /chat при ошибке делает session.rollback() и возвращает 503 с понятным
  сообщением — больше не остаётся «диалогов-призраков» с одной репликой;
- UI убирает свой пузырь и возвращает текст в поле ввода для повторной отправки.

UI «Настройки» — добавлена вкладка «Шаги» для веток с state machine: список
шагов chip-ами, редактор промпта/имени/allowed_next/guards, сохранение через
PATCH /intents/{code}/steps/{step_code} без версионирования. Иконка ⓘ возле
поля «Правила» открывает popover с пояснением, что туда писать.

UI «Песочница»:
- блок «Состояние диалога» показывает имя шага из intent_steps (а не сырое
  число), для не-sm-веток пишется «без пошагового сценария»;
- подсветка illegal-переходов (валидатор отклонил state_after) и parse_error
  для sm-веток;
- блок «Решение роутера» развёрнут в три исхода: «попал в ту же ветку» /
  «удержались в ветке» / «ветка сама передала управление через INTENT_CHANGE»;
- секция «Найденные фрагменты» сворачивается, карточки чанков раскрываются
  по клику — правый сайдбар стал компактнее.

Терминология (по договорённости — простой русский в UI):
- «тред» → «диалог» в текстах для оператора (в коде/API thread_id оставлен);
- «sticky state machine» → «удержались в ветке»;
- «state machine» → «пошаговый сценарий» в видимых местах.

SPRINTS.md: блок G в Спринте 6b сокращён — sticky-логика уже сделана здесь,
осталась только вторая линия (передача thread_state в системный промпт самого
роутера для ещё более точной первичной классификации).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:45:42 +05:00

101 lines
3.6 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, intent_service, intent_step_service # noqa: E402
from services.embeddings import EmbeddingService # noqa: E402
from services.llm_client import LLMClient # noqa: E402
from services.router_client import RouterClient # 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
router_client: RouterClient | 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, router_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()
router_client = RouterClient()
logger.info("LLM + Router clients ready (model=%s)", llm_client.model)
async with SessionLocal() as session:
await intent_service.ensure_seed_intents(session)
await config_service.migrate_legacy_config_to_general_info(session)
await config_service.ensure_seed_configs(session)
await intent_step_service.ensure_seed_steps(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, intents, 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.include_router(intents.router)
app.mount("/", StaticFiles(directory="static", html=True), name="static")