Files
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

106 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from db.session import get_session
from models.requests import QueryRequest
from models.responses import QueryResponse, SourceInfo
from services import config_service, intent_document_service, intent_service
from services.llm_client import LLMClient
from services.rag_pipeline import rag_query
logger = logging.getLogger(__name__)
router = APIRouter(tags=["query"])
DEBUG_INTENT_CODE = "_debug"
@router.post("/query", response_model=QueryResponse)
async def query_rag(request: QueryRequest, session: AsyncSession = Depends(get_session)):
from main import vectorstore_service
if vectorstore_service is None:
raise HTTPException(status_code=503, detail="Service not ready")
if not settings.deepseek_api_key:
raise HTTPException(status_code=500, detail="DEEPSEEK_API_KEY not configured")
# Дефолт ветки — _debug (страница «Отладки»). При тесте из Настроек оператор передаёт
# код выбранной ветки, и используются её промпт и подписки.
intent_code = request.intent_code or DEBUG_INTENT_CODE
intent = await intent_service.get_intent_by_code(session, intent_code)
if intent is None:
raise HTTPException(status_code=404, detail=f"Ветка {intent_code!r} не найдена.")
# Системный промпт: если override задан — используем его (тест черновика из Настроек),
# иначе — активный конфиг ветки. Если конфига нет и override пустой — 503.
if request.system_prompt is not None:
effective_system_prompt = request.system_prompt
config_version = None
else:
active_cfg = await config_service.get_active_config_for_intent(session, intent.id)
if active_cfg is None:
raise HTTPException(
status_code=503,
detail=(
f"У ветки {intent_code!r} нет активной версии промпта. "
f"Зайдите в Настройки → выберите ветку → создайте и активируйте промпт."
),
)
effective_system_prompt = active_cfg.system_prompt
config_version = active_cfg.version
# document_ids: приоритет — явный параметр запроса. Иначе берём подписки ветки.
# Для _debug дефолт пустой подписки — None (вся коллекция, удобство Отладки);
# для пациентских и системных веток дефолт пустой — [] (= 0 чанков).
if request.document_ids is not None:
effective_doc_ids = request.document_ids
subscribed_count = len(effective_doc_ids) if effective_doc_ids is not None else 0
else:
subscribed = await intent_document_service.list_documents_for_intent_code(
session, intent_code,
)
if subscribed:
effective_doc_ids = subscribed
elif intent_code == DEBUG_INTENT_CODE:
effective_doc_ids = None # вся коллекция — только для _debug
else:
effective_doc_ids = [] # 0 чанков для остальных
subscribed_count = len(subscribed)
# disable_rag — пропускаем retrieval целиком (для веток без RAG, например _router).
if request.disable_rag:
effective_doc_ids = [] # пустой список → vectorstore.query вернёт []
llm_client = LLMClient()
try:
result = await rag_query(
vectorstore=vectorstore_service,
llm_client=llm_client,
question=request.text,
top_k=request.top_k,
document_ids=effective_doc_ids,
temperature=request.temperature,
max_tokens=request.max_tokens,
system_prompt=effective_system_prompt,
)
except Exception as e:
logger.exception("RAG query failed")
raise HTTPException(status_code=500, detail=f"RAG query error: {e}")
return QueryResponse(
answer=result["answer"],
sources=[SourceInfo(**s) for s in result["sources"]],
model_used=result["model_used"],
assembled_prompt=result.get("assembled_prompt", ""),
intent_code=intent_code,
config_version=config_version,
rag_subscription={
"subscribed_count": subscribed_count,
"found_count": len(result["sources"]),
},
)