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