feat(chat): LLM tool-loop + §19 redaction for parcel chat (#957 Step2+3) #1173
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#1173
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/chat-957-llm-loop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Step 2 (+ folded Step 3) of #957 — the LLM prose-composition path for the parcel chat, layered over the Step-1 deterministic fallback. Dormant until
llm_enabled=True+ a prodOPENAI_API_KEY; default-off → deterministic path, no provider constructed, no network.§19 safety invariant (load-bearing)
safe_payload.build_chat_payloadis the single outboundSafePayloadconstructor; the onlycomplete()call in the chat package goes through it.fieldscan be populated only by the 5 read-only section tools (tools.py). The §22 report has 8 sections — the tools expose just 5;market_now(built from the rawanalyzeblob — competitors/prices/deals, highest-PII) andmetaare deliberately NOT exposed, so they cannot reach the LLM. A planted-secret test puts phone+ФИО inmarket_nowand proves it never appears in any outbound message.is_confidential=True→RedactionRefusedError→ fallback with zero provider calls.Tool-loop + grounding (§16)
Manual tool-call loop (no agent magic):
complete→ execute requested section slicers against the already-loaded report dict (no DB, no recompute, no engine math) → re-call until done orcall_indexhitsllm_max_calls_per_request→ compose. Numbers come from the report sections; the LLM only composes RU prose around them.grounded_in.sections= sections the model actually requested (honest provenance).Fallback-always
Every
LLMResult.ok=Falsereason (disabled / no_api_key / timeout / rate_limited / provider_error / redaction_refused / call_cap / empty_response) degrades to the Step-1 deterministicrender_answer(llm_used=False+fallback_reason). Empty LLM prose is guarded (never returned).Test plan
uv run pytest tests/services/chat/ tests/api/v1/test_chat.py -q→ 77 passed (fake providers only, zero network)ruff check+mypy(changed source files) → cleanmarket_now/metaunreachable, is_confidential→refused→fallback (0 provider calls), llm_enabled=False→no-provider tripwire, call-cap termination, per-reason fallback, numbers-from-report-not-inventedOPENAI_API_KEYin prod backend+worker env →LLM_ENABLED=true; then smokePOST /chat/askwith a real §22 run.Scope
Only
services/chat/*+ additivellm/prompts.pytemplate +api/v1/chat.py.parcels.py/special_indices.py/own_portfolio.pyuntouched; no pyproject/uv.lock drift.Next
#957 chat is functionally complete (deterministic live now; LLM path dormant-until-key). Remaining: frontend chat UI (separate frontend track).
Refs #957