Chat could not answer 'какая зона ПЗЗ?' — retrieval loaded only the §22
forecast run (schema 1.0); тер.зона/градрегламент/ЗОУИТ/ЕГРН live in the
analyze-1.0 run and never reached the chat context.
Add get_parcel_context_for_chat: a §19-safe whitelist slice of analyze
(fixed keys AND scalar-only values — dict/list dropped at the boundary;
ЗОУИТ zone names as a dedicated str-only capped list; no geometry/PII/raw
sub-dicts) merged into a copy of report_dict under 'parcel_context';
expose it via the sixth sectional tool get_parcel_info. Bump chat_system
prompt v2->v3 so the model uses it and no longer treats the parcel's own
ПЗЗ zone as out of scope.
Add the LLM prose-composition path for the parcel-forecast chat, layered
over the deterministic Step-1 fallback which stays the safety net.
- chat/tools.py: 5 read-only section tools (exec_summary, product_recommendation,
forecast, risks, scenarios) — pure slices of the loaded report dict, no DB/
recompute, graceful on missing sections. market_now (raw analyze blob) and meta
are deliberately NOT exposed -> highest-PII data cannot reach the LLM.
- chat/safe_payload.py: the §19 gate — single place that builds the outbound
SafePayload from a section-aggregate allowlist; honors is_confidential hard-block.
- chat/orchestrator.py: manual tool-call loop with call-cap/termination, real
grounded_in provenance; any LLMResult.ok=False (disabled/timeout/rate_limited/
redaction_refused/call_cap/provider_error/empty) degrades to the deterministic answer.
- llm/prompts.py: versioned chat_system@v1 — answer only from sections, never
fabricate numbers, advisory tone, decline out-of-scope.
- api/v1/chat.py: branch on settings.llm_enabled; sync complete bridged via
run_in_threadpool. Default-off -> deterministic path, no provider built.
- Tests: fake provider only (no network), planted-secret redaction-boundary +
per-reason fallback + call-cap + numbers-from-report coverage.
Refs #957