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

11 KiB

#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