gendesign/backend/app/services/chat/retrieval.py
Light1YT 4847a2eb01
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (pull_request) Successful in 6m23s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m25s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m43s
Deploy / deploy (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 7m39s
feat(chat): deterministic §22 chat foundation — POST /api/v1/chat/ask (no LLM)
Step 1 of #957. Answers parcel questions by reading the already-persisted
§22 SiteFinderReport (latest_run_for, schema "1.0", read-only) and returning
templated RU answers with engine numbers verbatim (§16, never fabricated).
Intent routing (explicit or RU keyword match) -> per-section renderers;
graceful on partial/missing sections and pending (no run / DB error) without
500s. Works with llm_enabled=False (llm_used always False); LLM composition
is Step 2. Mounted off /api/v1/chat so rbac_guard auto-requires an authed
known user.

Refs #957
2026-06-08 17:21:51 +05:00

59 lines
3 KiB
Python
Raw 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.

"""Read-only достаёт персистентный §22-форсайт участка для чата (#957, Step 1).
Тонкая обёртка над `latest_run_for(..., schema_version="1.0")` — ровно тот же seam,
что читает `GET /{cad_num}/forecast` (parcels.py): последний ран ИМЕННО §22-схемы
('1.0', НЕ analyze-1.0 inline-dict). `run.result` — это `SiteFinderReport.as_dict()`
(стабильный 8-секционный контракт), `run.id` — run_id для grounding.
Q2-РЕШЕНИЕ = READ-ONLY: рана нет → возвращаем (None, None). Эндпоинт по этому отдаёт
report_status='pending' + детерминированный RU-ответ «отчёт ещё не готов, запустите
анализ участка». НИЧЕГО не считаем, НЕ enqueue'им, НЕ пере-вычисляем (чат не триггерит
форсайт — это делает POST /analyze). Сбой БД пробрасываем НЕ из этого модуля —
гасит/логирует его вызывающий эндпоинт (как get_parcel_forecast → pending).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy.orm import Session
from app.services.analysis_runs.repository import latest_run_for
logger = logging.getLogger(__name__)
# Версия схемы §22-форсайта (зеркало _FORECAST_SCHEMA_VERSION в parcels.py). Читаем
# именно её, а не analyze-1.0: view вернул бы §22-ран поверх analyze, но фильтр по
# схеме явный и устойчивый.
_FORECAST_SCHEMA_VERSION = "1.0"
def get_report_for_chat(
db: Session,
cad_num: str,
) -> tuple[dict[str, Any] | None, int | None]:
"""Достать последний §22-отчёт участка как (report_dict, run_id) или (None, None).
READ-ONLY (Q2): рана нужной схемы нет → (None, None), БЕЗ enqueue/пере-расчёта.
`run.result` — `SiteFinderReport.as_dict()` (8 секций). Если result почему-то не
dict (мусор/частичная запись) — логируем и отдаём (None, None) как «нет отчёта»,
чтобы чат не упал (graceful).
"""
run = latest_run_for(db, cad_num, schema_version=_FORECAST_SCHEMA_VERSION)
if run is None:
return None, None
result = run.result
if not isinstance(result, dict):
# Ран есть, но result не похож на отчёт (старая/битая запись) — не валим чат,
# трактуем как «отчёта ещё нет» (pending). Логируем для диагностики.
logger.warning(
"chat: run %s for cad=%s has non-dict result (%s) — treating as pending",
run.id,
cad_num,
type(result).__name__,
)
return None, None
return result, run.id