== 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
104 lines
11 KiB
Markdown
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
|