Files
RAG_helper/main.py
T
AR 15 M4 52b46bc53e feat(sprint6c+sprint7): терминология, сверка примеров с кодом, мульти-RAG (часть A)
Спринт 6c — терминология и сверка документации с реальным кодом:
- Словарь терминов в static/docs.html: «маршрутизатор» вместо «роутер»,
  «защитное условие» вместо «guard», «пошаговая ветка» вместо «многошаговая».
  Разделены концепты «намерение» (intent) и «ветка» (branch) с пометкой,
  что в коде они хранятся как одна сущность 1:1.
- Песочница: «Решение маршрутизатора» виден всегда (зелёный/жёлтый),
  счётчик переключений «N из 3» отдельной плашкой, бейджи под словарь.
- Настройки: «Условия перехода» → «Защитные условия (guards, JSON)».
- GRAPH_ARCHITECTURE_v4.md: имена полей thread_state и слоты приведены
  к реальной БД (db/models/thread_state.py) и таксономии промптов шагов
  (prompts/intents/new_booking/steps/). Ссылки на *_v2 примеры. На v3
  поставлена шапка «устарело».
- 4 примера переписаны как *_v2: реальные current_intent_code/
  current_step_code/slots_json, реальные allowed_next без двойных переходов,
  реальная таксономия слотов name/reason/specialist/preferred_time/confirmed.
  Удалены вымышленные CRM tool calls и слоты, которых нет в коде.
- static/example.html — параметризованная страница с навигацией между
  4 примерами; роут GET /api/docs/examples/{name} в main.py отдаёт
  markdown без дублирования файлов.
- Редактирование документов в Отладке: GET/PUT /documents/{id}/raw,
  textarea с переразметкой и обновлением Chroma при сохранении.

Спринт 7, часть A — мульти-RAG через подписку ветка↔документы:
- Миграция: таблица intent_documents (M:N), модель IntentDocument,
  индекс по document_id для обратного поиска.
- API: GET/PUT /intents/{code}/documents и GET/PUT /documents/{id}/intents
  с PUT-семантикой «полный список», атомарно. Сервис
  services/intent_document_service.py.
- Retrieval-фильтр в chat_service: подтягивает document_ids активной
  ветки и передаёт в vectorstore.query(). Дефолт пустой подписки —
  document_ids=[] (= 0 чанков), не «вся коллекция»: пустая подписка
  означает «ветка не настроена», подмешивать случайное хуже, чем
  ничего. vectorstore.query() различает None (нет фильтра) и [] (0).
- UI Настроек: блок «Документы базы знаний» в правом сайдбаре,
  всегда видим независимо от вкладки, сортировка по имени, счётчик
  «N из M», PUT при сохранении.
- UI Отладки: третья кнопка «привязка» рядом с «удалить» —
  раскрывашка со списком веток (галочки), быстрая привязка прямо
  на странице загрузки.
- Песочница: блок «Срез RAG» с подпиской/найдено, ворнинг при пустой
  подписке. Поле rag_subscription в QueryResponse и ChatResponse.
- Системный промпт страницы Отладки переехал в обычную ветку _debug
  («Страница отладки»). Удалён prompts/system_prompt.md и логика
  DEFAULT_SYSTEM_PROMPT в llm_client. routers/query.py подтягивает
  активный конфиг ветки _debug и её подписки. Дефолт пустой подписки
  для _debug — None (вся коллекция), не [] как для пациентских — чтобы
  Отладка работала «из коробки». На странице Отладки info-bar показывает
  активную версию и счётчик подписок, ссылка → Настройки.
- Тест-блок «Тест-вопрос» в центре Настроек: расширил /query
  параметрами intent_code (default _debug), system_prompt (override
  для теста черновика из textarea), disable_rag (для _router).
  Редактор промпта обёрнут в <details open> — можно свернуть до
  одной строки. Под ним — три колонки результата (RAG / промпт /
  ответ). Для _router показывается подсказка про отсутствие RAG.

Документы:
- data/datasets/*.md — наработки по 6 веткам (рабочие материалы оператора).
- docs/BRANCH_MAP_AND_PROMPTS_v1.md, docs/OPTIMIZATION_CONVERSION_v1.md,
  docs/guides/state_machine_and_slots.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:00:44 +05:00

117 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, 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.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")