== 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
11 KiB
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
/agentpage. 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.sveltereads URL params inonMount.updateActiveEnv()syncs_activeEnvIdANDuiContext.envId. NoinitialContextin store. Context captured once, not per-message.
2. Gradio Payload — uicontext at END
- Decision:
uicontext_strappended 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=Noneas 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()intools.py.tool_retrySSE 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()intools.py. AGTL-FR-003 updated.
7. Write Tool Timeout Safety
- Decision: Write/mutating tool timeout produces
tool_timeoutwithis_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()acceptsis_writeparam.tool_timeoutSSE extended withis_write_toolfield.
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_capabilitiesalways included. WhenobjectType=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 forobjectType=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 inapp.pyextended to acceptobjectTypeparam.
3. Tool Selection Pipeline
- Decision: Ordered pipeline: (1) RBAC role-based exclusion, (2) context-type hard filter.
show_capabilitiesalways 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 topermission_deniedSSE (NOTconfirm_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_requiredwithpermission_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=truein 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.sveltegains$state(dangerousTimer)+setInterval.AgentChatModelgainsdangerousCountdown/dangerousCountdownActiveatoms.
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_errorevent withretry_exhausted:true. Frontend ToolCallCard showsretryingstate 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.pygains_retry_wrapper(). New SSE event types:tool_retry, extendedtool_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()intools.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-devto/agent?objectType=dashboard&objectId=42&envId=ss-dev, the/agentpage reads URL params inonMountand callsmodel.setUIContextFromParams(). Context pill in process steps shows «📋 Дашборд #42 · ss-dev». Env can be changed via EnvSelector on the agent page —model.updateActiveEnv()updatesisProdContextreactively. - 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.svelteonMountreads URL params.assistantChatStoreextended withinitialContext. 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