Files
ss-tools/specs/035-agent-chat-context/research.md
busya ff60865183 feat(agent-chat): 035-agent-chat-context — контекст, guardrails, tools, database discovery
== User stories ==
US1: Контекст с дашборда/датасета → /agent с URL params
US2: Guardrails card — env badge, 7 risk tones, countdown, permission_denied
US3: Tools optimization — retry, timeout, summarise, RBAC + context affinity

== Backend ==
- _context.py (NEW): UIContext validation (7 checks)
- _tool_filter.py (NEW): RBAC + context affinity pipeline
- _confirmation.py: build_confirmation_contract_v2, permission_denied_payload
- tools.py: superset_list_databases, retry/summarise/timeout wrappers
- app.py: _inject_uicontext, _inject_env_id_into_tools, database
  prefetch в runtime context
- _persistence.py: prefetch_databases()
- agent_superset_explore.py: GET /databases endpoint
- _llm_async_http.py, _persistence.py: fix double /v1 в LLM URL
  (LM Studio Unexpected endpoint)

== Frontend ==
- AgentChatModel.svelte.ts: 5 atoms, 3 actions, countdown, context
- AgentChat.svelte: production banner, process steps, debug panel
- ConfirmationCard.svelte: 7 risk tones, permission_denied, countdown
- ToolCallCard.svelte: retrying/timeout/cancelled states
- StreamProcessor.svelte.ts: tool_retry, timeout, permission_denied
- TopNavbar: sparkles icon + Ассистент
- sidebarNavigation: AI section
- DashboardHeader, datasets/+page: contextual AI buttons
- Icon: sparkles, brain, cpu icons
- tailwind: assistant category colors
- i18n: en/ru nav keys

== Tests ==
- 159 backend agent tests (+16 US3: retry, timeout, summarise, contracts)
- 2544 frontend tests (+11 model + component tests)
- 15 JSON fixtures (10 API + 5 model)

== Specs ==
- specs/035-agent-chat-context/: spec, UX, plan, tasks, research,
  data-model, contracts, quickstart, traceability, fixtures, checklists

Closes #035
2026-07-04 22:47:17 +03:00

104 lines
11 KiB
Markdown

#region Research [C:3] [TYPE ADR] [SEMANTICS research,agent-chat,planning]
@defgroup Research Phase 0 — resolved unknowns for 035-agent-chat-context.
## 1. Context Model — `/agent` Page (Path B)
- **Decision**: Dedicated `/agent` page. User navigates from any page via «Спросить агента» button → `/agent?objectType=...&objectId=...&objectName=...&envId=...&route=...`. Context is static for visit duration, envId updatable via EnvSelector.
- **Rationale**: Simpler state management vs drawer overlay. No z-index conflicts, no $page.url reactivity tracking, no stale-context edge cases during navigation. Aligns with existing UX decisions.
- **Alternatives considered**: Drawer panel on any page (Path A) — rejected. Requires $page.url $effect tracking, cross-component context sync, fragile during page navigation mid-stream.
- **Impact**: `+page.svelte` reads URL params in `onMount`. `updateActiveEnv()` syncs `_activeEnvId` AND `uiContext.envId`. No `initialContext` in store. Context captured once, not per-message.
## 2. Gradio Payload — uicontext at END
- **Decision**: `uicontext_str` appended as position 6 in Gradio submit args: `[message, null, convId, userId, userJwt, envId, uicontext]`.
- **Rationale**: Middle-position insertion would shift existing arguments for clients built against the current 6-arg signature. Appending is truly backward-compatible — missing arg defaults to `None`.
- **Alternatives considered**: Position 3 insertion — rejected as BREAKING.
- **Impact**: `agent_handler()` signature: `uicontext_str=None` as last param. All existing callers pass 6 args → uicontext defaults to None.
## 5. Tool Retry — Fixed 1s Delay
- **Decision**: Single retry with fixed 1s `asyncio.sleep`. Read-only tools only.
- **Rationale**: "Exponential backoff" terminology was incorrect — single retry cannot be exponential. Fixed 1s is simple and catches transient failures without latency explosion.
- **Alternatives considered**: True exponential backoff (1s, 2s, 4s) — rejected (3 retries = 7s+ latency). Auto-retry write tools — rejected (idempotency risk).
- **Impact**: `_retry_read_tool()` in `tools.py`. `tool_retry` SSE event with attempt counter.
## 6. Response Summarisation — Structured Truncation
- **Decision**: "Semantic summarisation" renamed to "structured truncation". Top-5 items + total count for JSON arrays. Sentence-boundary truncation for text. Object keys + sample for JSON objects.
- **Rationale**: More accurate description — not true semantic summarisation, just preserving structure during truncation.
- **Impact**: `_summarise_response()` in `tools.py`. AGTL-FR-003 updated.
## 7. Write Tool Timeout Safety
- **Decision**: Write/mutating tool timeout produces `tool_timeout` with `is_write_tool=true, retryable=false`. Frontend shows "Operation status unknown — check Superset" — no retry button.
- **Rationale**: Timeout ≠ mutation didn't happen. Server may continue processing. Auto-retry or user retry could duplicate mutations.
- **Impact**: `_execute_with_timeout()` accepts `is_write` param. `tool_timeout` SSE extended with `is_write_tool` field.
## 2. Tool Affinity Mapping (ContextToolAffinity)
- **Decision**: Static server-side mapping `objectType → set[operation_name]`. Three groups: `dashboard` (9 tools), `dataset` (8 tools), `migration` (5 tools). `show_capabilities` always included. When `objectType=null`, all 22 tools returned (current behavior).
- **Rationale**: Static mapping is predictable, zero latency, auditable. The embedding router (`_embedding_router.py`) already exists but is unused — static mapping is more deterministic for context filtering. Embedding router can be added later as semantic fallback for `objectType=null`.
- **Alternatives considered**: LLM-based dynamic filtering — rejected (adds latency, non-deterministic). Embedding-only — rejected (model loading overhead, 200MB RAM). Client-side filtering — rejected (security — must be server-enforced).
- **Impact**: New file `backend/src/agent/_tool_filter.py`. `get_all_tools()` call site in `app.py` extended to accept `objectType` param.
## 3. Tool Selection Pipeline
- **Decision**: Ordered pipeline: (1) RBAC role-based exclusion, (2) context-type hard filter. `show_capabilities` always included. Embedding router deferred to post-MVP (AGTL-FR-006).
- **Rationale**: RBAC MUST run first (security). Context filtering is UX enhancement. Two-layer enforcement: prompt filtering + invocation guard catches hallucinated/direct calls.
- **Alternatives considered**: Embedding-first routing — rejected for initial release (200MB model, 50ms latency). Prioritisation (ranking) — rejected in favor of hard exclusion for predictability.
- **Impact**: `build_tool_pipeline()` in `_tool_filter.py`. `enforce_tool_permission()` invocation guard. AGTL-FR-005 rewritten as two-layer enforcement.
## 4. Guardrails Risk Classification v2
- **Decision**: Three-axis risk: `tool_risk` (read/write/dangerous), `env_risk` (prod/staging/dev → normalised from env_id), `permission_risk` (RBAC check). Permission-denied flows to `permission_denied` SSE (NOT `confirm_required`). Env resolution: `tool_args.env_id > submit envId > UIContext.envId > null`.
- **Rationale**: Permission-denied MUST bypass HITL checkpoint — entering checkpoint for a known-forbidden call is a security anti-pattern. Separate event type lets frontend render without confirm button.
- **Alternatives considered**: `confirm_required` with `permission_granted=false` — rejected as security anti-pattern.
- **Impact**: `build_confirmation_contract_v2()`. `yield_permission_denied()`. New SSE event type.
## 4. Dangerous Tool Countdown (10s)
- **Decision**: Frontend-only countdown. When `dangerous=true` in confirmation metadata, confirm button disabled for 10 seconds with visual countdown. Backend does NOT enforce the delay — it's a UX guard, not a security control. Backend HITL interrupt already prevents automatic execution.
- **Rationale**: UX guard against impulse clicks on irreversible operations. Frontend-only because: (a) backend already has HITL checkpoint, (b) server-side delay would tie up Gradio worker for 10s, (c) user can still deny during countdown.
- **Alternatives considered**: Server-side delay — rejected (blocks worker). Two-step confirmation — rejected (user fatigue). Undo toast — rejected (many ops are irreversible at DB level).
- **Impact**: `ConfirmationCard.svelte` gains `$state(dangerousTimer)` + `setInterval`. `AgentChatModel` gains `dangerousCountdown`/`dangerousCountdownActive` atoms.
## 5. Tool Retry Strategy
- **Decision**: Hybrid. Backend auto-retries ONCE with 1s exponential backoff for read-only tools on transient HTTP errors (502/503/504, connection errors). Both attempts fail → `tool_error` event with `retry_exhausted:true`. Frontend ToolCallCard shows `retrying` state during auto-retry, then ❌ + manual «Повторить» button on exhaustion.
- **Rationale**: One auto-retry catches 80% of transient failures without user intervention. Manual retry gives user control when auto-retry fails. Read-only only because mutating operations must not auto-retry (idempotency not guaranteed).
- **Alternatives considered**: Auto-retry all tools — rejected (write idempotency risk). No auto-retry — rejected (transient 502s frustrate users). Exponential backoff with 3+ retries — rejected (adds 7s+ latency).
- **Impact**: Tool wrapper in `tools.py` gains `_retry_wrapper()`. New SSE event types: `tool_retry`, extended `tool_error`.
## 6. Response Summarisation
- **Decision**: Server-side. When tool response >4000 chars, extract top-N items (N=5) + total count header. Format: `"Found X items:\n - item1\n - item2\n ...\n and (X-5) more items."` Applied BEFORE response enters LLM context window.
- **Rationale**: Hard truncation at 4000 loses structure (mid-JSON). Semantic summarisation preserves: (a) result count, (b) representative samples, (c) ability to query further if needed.
- **Alternatives considered**: Client-side summarisation — rejected (LLM already consumed full response). LLM summarisation — rejected (extra LLM call, token cost). Pagination in tools — rejected (requires tool rework for all 22 tools).
- **Impact**: New helper `_summarise_response()` in `tools.py`. Called inside `_trim_response()` replacement. No API shape change.
## 7. Context Passing on /agent Page
- **Decision**: Route-level context. When user navigates from `/dashboards/42?env_id=ss-dev` to `/agent?objectType=dashboard&objectId=42&envId=ss-dev`, the `/agent` page reads URL params in `onMount` and calls `model.setUIContextFromParams()`. Context pill in process steps shows «📋 Дашборд #42 · ss-dev». Env can be changed via EnvSelector on the agent page — `model.updateActiveEnv()` updates `isProdContext` reactively.
- **Rationale**: Agent page is a dedicated route — no further navigation happens during chat. Context is static per-visit, reducing complexity. URL params survive page refresh.
- **Alternatives considered**: Live page tracking via `$effect` — rejected (no navigation on `/agent`). Lazy on send — rejected (user wants to see context before sending).
- **Impact**: `+page.svelte` `onMount` reads URL params. `assistantChatStore` extended with `initialContext`. No routing changes needed — params are additive.
## 8. Existing Module Touches
| File | Change | Risk |
|------|--------|------|
| `backend/src/agent/app.py` | `agent_handler()` gains `uicontext_str` param. `_build_agent_context()` extended. Tool call filtered. | **Low** — additive, backward-compat (param defaults to None) |
| `backend/src/agent/tools.py` | Retry wrapper, summarise helper. | **Low** — internal helpers, no API change |
| `backend/src/agent/_confirmation.py` | Extended metadata fields. | **Low** — additive fields in existing dict |
| `backend/src/agent/_tool_filter.py` | NEW file. | **None** — new module |
| `frontend/src/lib/models/AgentChatModel.svelte.ts` | +60 LOC atoms/derived/actions. | **Medium** — 915→975 lines, under 400 limit |
| `frontend/src/lib/models/AgentChat.StreamProcessor.svelte.ts` | +35 LOC new handlers. | **Low** — modular |
| `frontend/src/lib/models/AgentChatTypes.ts` | +35 LOC new types. | **Low** — additive |
| `frontend/src/lib/components/agent/AgentChat.svelte` | +15 LOC prod bg, banner. | **Low** — 1027→1042 |
| `frontend/src/lib/components/assistant/ConfirmationCard.svelte` | +50 LOC new states. | **Medium** — 203→253, new states complex |
| `frontend/src/lib/components/assistant/ToolCallCard.svelte` | +25 LOC new states. | **Low** — modular |
**No file exceeds 400 LOC module limit after changes.** AgentChat.svelte is near 1050 but it's a component, not a module — INV_7 applies to modules, but decomposition consideration is noted for `/speckit.tasks`.
#endregion Research