74befa484d
Промпты веток (по docs/BRANCH_MAP_AND_PROMPTS_v1.md):
- reschedule.md — полная замена. Одношаговый сценарий из 6 пунктов:
action (cancel/reschedule), patient_name, patient_phone, original_time,
preferred_new_time. Слоты хранит вызывающая система, STATE_JSON не используется.
- price_question.md — добавлены 3 пункта: эндоскопия 1000₽ при первичном
ЛОР-приёме, лечебные процедуры доплачиваются, ОМС только сурдолог
(последний пункт работает только при подтверждении в базе).
- medical_question.md — расширена карта жалоб → специалист (ЛОР / сурдолог /
аллерголог / иммунолог / пульмонолог); добавлен пункт про беременность,
онкологию, психиатрию — мягко сказать «специализированная клиника»,
не предлагать запись.
- general_info.md — добавлены разделы «Отзывы и социальное доказательство»,
«Преимущества клиники», «Сокращения». Условия выхода расширены до 5 интентов.
escalate_human и new_booking не трогаем (escalate — карта говорит «не менять»;
new_booking — отдельный Спринт 7.6 по docs/OPTIMIZATION_CONVERSION_v1.md).
Применение в БД — вручную через UI «Настройки» (вариант A): оператор копирует
текст из .md, сохраняет как новую версию + активирует. Файлы — только seed.
Eval-каркас (заготовка под Спринт 8):
- eval/router_cases_booking.jsonl (875 кейсов new_booking) и
eval/router_cases_other.jsonl (698 кейсов: general_info 295, price 165,
escalate 139, medical 59, reschedule 40). CSV-исходники рядом.
- eval/README.md — формат, глоссарий, что это и зачем.
- routers/eval.py: GET /eval/router-cases?intent_code=...&limit=...
Lazy-кэш, сортировка по count desc, фильтр по expected_intent.
UI Настроек — выбор готового кейса в тест-блоке:
- Полоса «Готовый кейс:» с datalist (поиск по началу строки) + кнопка
«🎲 Случайный» + счётчик кейсов для активной ветки.
- При выборе — текст подставляется в textarea вопроса.
- Загружается при выборе ветки. Если кейсов 0 (для _router, _debug) — скрыто.
- Полная подсистема прогона (run.py, отчёты, baseline) — Спринт 8.
SPRINTS.md:
- Спринт 7 (мульти-RAG, часть A) → ✅ Закрыт (коммит 52b46bc).
- Заведён Спринт 7.5 «Обновление промптов 4 веток» (этот спринт).
- Заведён Спринт 7.6 «Оптимизация воронки new_booking до 4 шагов»
по OPTIMIZATION_CONVERSION_v1.md.
- В идеи на потом: сквозные правила всех веток (BRANCH_MAP §2),
отложенная документация Спринта 7 (docs.html карточка термина,
GRAPH_ARCHITECTURE_v5, README про мульти-RAG).
Также: docs/COMPETITOR_ALEXANDRA_top100.md — рабочие материалы пользователя
по конкурентному боту (NEXTBOT/Александра), используется как baseline для
оптимизации воронки в Спринте 7.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
4.3 KiB
Python
118 lines
4.3 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, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
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 intent_service.migrate_intent_copy(session)
|
|
await config_service.migrate_legacy_config_to_general_info(session)
|
|
await config_service.ensure_seed_configs(session)
|
|
await config_service.migrate_exit_conditions_to_field(session)
|
|
await intent_step_service.ensure_seed_steps(session)
|
|
await intent_step_service.ensure_seed_guards(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, eval as eval_router, 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.include_router(eval_router.router)
|
|
|
|
|
|
@app.get("/api/docs/examples/{name}")
|
|
def get_example_markdown(name: str):
|
|
safe = "".join(c for c in name if c.isalnum() or c in "_-")
|
|
if safe != name:
|
|
raise HTTPException(status_code=400, detail="invalid example name")
|
|
path = os.path.join("docs", "examples", f"{safe}.md")
|
|
if not os.path.isfile(path):
|
|
raise HTTPException(status_code=404, detail="example not found")
|
|
return FileResponse(path, media_type="text/markdown; charset=utf-8")
|
|
|
|
|
|
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|