From a7fb06cd8edaeb525562ac48cbf6e9105d46b9bf Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 9 Jun 2026 09:43:34 +0300 Subject: [PATCH] tasks ready 1 --- .kilocode/rules/specify-rules.md | 3 +- .opencode/command/speckit.plan.md | 132 +++++++ .opencode/command/speckit.tasks.md | 26 ++ .opencode/command/speckit.ux.md | 366 ++++++++++++++++++ .specify/memory/constitution.md | 3 +- .specify/templates/plan-template.md | 11 +- .specify/templates/tasks-template.md | 4 + .../src/lib/models/AgentChatModel.svelte.ts | 325 ++++++++++++++++ merge_spec.py | 154 ++++++-- .../checklists/requirements.md | 85 ++++ .../contracts/modules.md | 30 ++ .../contracts/ux/agent-chat-ux.md | 88 +++++ .../contracts/ux/api-ux.md | 90 +++++ .../contracts/ux/decisions.md | 48 +++ .../contracts/ux/design-tokens.md | 113 ++++++ .../contracts/ux/screen-models.md | 29 ++ specs/033-gradio-agent-chat/data-model.md | 52 +++ specs/033-gradio-agent-chat/plan.md | 71 ++++ specs/033-gradio-agent-chat/quickstart.md | 57 +++ specs/033-gradio-agent-chat/research.md | 52 +++ specs/033-gradio-agent-chat/spec.md | 133 +++++++ specs/033-gradio-agent-chat/tasks.md | 319 +++++++++++++++ specs/033-gradio-agent-chat/ux_reference.md | 33 ++ 23 files changed, 2179 insertions(+), 45 deletions(-) create mode 100644 .opencode/command/speckit.ux.md create mode 100644 frontend/src/lib/models/AgentChatModel.svelte.ts create mode 100644 specs/033-gradio-agent-chat/checklists/requirements.md create mode 100644 specs/033-gradio-agent-chat/contracts/modules.md create mode 100644 specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md create mode 100644 specs/033-gradio-agent-chat/contracts/ux/api-ux.md create mode 100644 specs/033-gradio-agent-chat/contracts/ux/decisions.md create mode 100644 specs/033-gradio-agent-chat/contracts/ux/design-tokens.md create mode 100644 specs/033-gradio-agent-chat/contracts/ux/screen-models.md create mode 100644 specs/033-gradio-agent-chat/data-model.md create mode 100644 specs/033-gradio-agent-chat/plan.md create mode 100644 specs/033-gradio-agent-chat/quickstart.md create mode 100644 specs/033-gradio-agent-chat/research.md create mode 100644 specs/033-gradio-agent-chat/spec.md create mode 100644 specs/033-gradio-agent-chat/tasks.md create mode 100644 specs/033-gradio-agent-chat/ux_reference.md diff --git a/.kilocode/rules/specify-rules.md b/.kilocode/rules/specify-rules.md index 6ba0afba..e5745c83 100644 --- a/.kilocode/rules/specify-rules.md +++ b/.kilocode/rules/specify-rules.md @@ -9,6 +9,7 @@ Auto-generated from all feature plans. Last updated: 2026-05-08 - PostgreSQL 16 (dedicated ss-tools DB — not Superset metadata DB per ADR-0003) (031-maintenance-banner) - Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI 0.126, SQLAlchemy, APScheduler 3.11, httpx 0.28 (already present), anyio 4.12 (already present) (032-translate-requests-httpx) - PostgreSQL 16 (unchanged — DB operations via asyncio.to_thread) (032-translate-requests-httpx) +- Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI, SQLAlchemy, Gradio ≥5.0, LangChain ≥0.3, langchain-openai (backend); SvelteKit 5, Vite, Tailwind CSS, @gradio/client (frontend) (033-gradio-agent-chat) - Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5) + FastAPI 0.115+, SQLAlchemy 2.0+, APScheduler 3.x, Pydantic v2 (backend); SvelteKit 2.x, Svelte 5.43+, Vite 7.x, Tailwind CSS 3.x (frontend) (028-llm-datasource-supeset) @@ -29,9 +30,9 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5): Follow standard conventions ## Recent Changes +- 033-gradio-agent-chat: Added Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI, SQLAlchemy, Gradio ≥5.0, LangChain ≥0.3, langchain-openai (backend); SvelteKit 5, Vite, Tailwind CSS, @gradio/client (frontend) - 032-translate-requests-httpx: Added Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI 0.126, SQLAlchemy, APScheduler 3.11, httpx 0.28 (already present), anyio 4.12 (already present) - 031-maintenance-banner: Added Python 3.9+ (backend), JavaScript/TypeScript (frontend Svelte 5 runes) + FastAPI 0.126, SQLAlchemy 2.0, APScheduler 3.11 (backend); SvelteKit 2.x, Svelte 5.43, Vite 7.x, Tailwind CSS 3.x (frontend) -- 030-dataset-lifecycle-workspace: Added Python 3.9+ (backend), JavaScript/TypeScript — Svelte 5 runes (frontend) + FastAPI 0.104+, Pydantic v2, SQLAlchemy (backend); SvelteKit 2.x, Svelte 5.x, Vite 7.x, Tailwind CSS 3.x (frontend) diff --git a/.opencode/command/speckit.plan.md b/.opencode/command/speckit.plan.md index 19a89a09..4c500848 100644 --- a/.opencode/command/speckit.plan.md +++ b/.opencode/command/speckit.plan.md @@ -36,6 +36,9 @@ You **MUST** consider the user input before proceeding (if not empty). - `.opencode/skills/semantics-svelte/SKILL.md` - `.opencode/skills/semantics-testing/SKILL.md` - `.specify/templates/plan-template.md` + - `FEATURE_DIR/contracts/ux/screen-models.md` (if `/speckit.ux` was run) + - `FEATURE_DIR/contracts/ux/api-ux.md` (if `/speckit.ux` was run) + - `FEATURE_DIR/contracts/ux/*-ux.md` (per-screen UX contracts) - relevant `docs/adr/*.md` 3. **Execute the planning workflow** using the template structure: @@ -44,6 +47,7 @@ You **MUST** consider the user input before proceeding (if not empty). - ERROR if a blocking constitutional or semantic conflict is discovered and cannot be justified. - Phase 0: generate `research.md` in `FEATURE_DIR`, resolving all material unknowns. - Phase 1: generate `data-model.md`, `contracts/modules.md`, optional machine-readable contract artifacts, and `quickstart.md` in `FEATURE_DIR`. + - Phase 1: if UX contracts exist, generate `traceability.md` — a requirements traceability matrix mapping Story → Model → API → Task → Test. - Materialize blocking ADR references and planning decisions inside the plan and downstream contracts. - Run `.specify/scripts/bash/update-agent-context.sh kilocode` after planning artifacts are written. @@ -63,6 +67,12 @@ Research must resolve only implementation-shaping unknowns that matter for this - belief runtime instrumentation for C4/C5 flows - semantic validation boundaries and static verification workflow +**If `/speckit.ux` was run before plan:** +- `screen-models.md` defines Model inventory → use directly, don't re-discover +- `api-ux.md` defines API shapes → use as @DATA_CONTRACT source for backend Pydantic schemas +- `-ux.md` defines UX contracts → use as @UX_STATE/@UX_FEEDBACK source for component contracts +- Generated `.svelte.ts` model files in `frontend/src/lib/models/` → DO NOT regenerate; reference them via `@RELATION BINDS_TO` from component contracts + Write `research.md` with concise sections: - Decision - Rationale @@ -230,6 +240,93 @@ Generate its full `#region` header in `contracts/modules.md` under its parent mo If a planned contract depends on unknown schema, relation target, or ADR identity, emit `[NEED_CONTEXT: target]` instead of fabricating placeholders. +### Fixture Generation (MANDATORY for C3+ contracts with @TEST_EDGE) + +For every C3+ contract that declares `@TEST_EDGE`, `@POST`, or `@REJECTED` guardrails, generate **canonical test fixtures** in `FEATURE_DIR/fixtures/`. Canonical fixtures live beside the spec — they are the design-time source of truth. Executable fixtures are materialized into `tests/` later by `/speckit.tasks`. + +**Output structure:** + +```text +specs//fixtures/ +├── manifest.md # Fixture index with GRACE contracts +├── api/ +│ ├── _valid.json +│ ├── _missing_field.json +│ ├── _invalid_type.json +│ ├── _external_fail.json +│ └── _rejected_path.json +└── model/ + ├── _valid.json + ├── _edge_case.json + └── _invariant.json +``` + +**`manifest.md` — fixture index with GRACE contracts:** + +```markdown +#region FixtureManifest [C:3] [TYPE ADR] [SEMANTICS test,fixture,[DOMAIN]] +@defgroup Fixtures Canonical test fixtures for [FEATURE]. + +## @{ Fixture FX_Auth.Login.Valid [C:2] [TYPE Block] [SEMANTICS test,auth,fixture] +@BRIEF Valid login request/response pair. +@RELATION VERIFIES -> [Api.Auth.Login] +@TEST_FIXTURE: valid_login -> fixtures/api/auth_login_valid.json +@TEST_INVARIANT: TokenIssued -> VERIFIED_BY: [Test.Api.Auth] +## @} Fixture FX_Auth.Login.Valid + +## @{ Fixture FX_Auth.Login.MissingPassword [C:2] [TYPE Block] [SEMANTICS test,auth,fixture] +@BRIEF Missing password field — @TEST_EDGE: missing_field. +@RELATION VERIFIES -> [Api.Auth.Login] +@TEST_EDGE: missing_field -> 422 VALIDATION_ERROR +@TEST_FIXTURE: missing_password -> fixtures/api/auth_login_missing_field.json +## @} Fixture FX_Auth.Login.MissingPassword + +## @{ Fixture FX_Migration.EnvReset [C:2] [TYPE Block] [SEMANTICS test,migration,fixture] +@BRIEF Model invariant: changing source env resets selection. +@RELATION VERIFIES -> [Migration.Model] +@TEST_INVARIANT: env_reset_selection -> VERIFIED_BY: [Test.Migration.Model] +@TEST_FIXTURE: env_reset -> fixtures/model/migration_env_reset.json +## @} Fixture FX_Migration.EnvReset +``` + +**JSON fixture format:** + +```json +{ + "fixture_id": "FX_Auth.Login.MissingPassword", + "verifies": "Api.Auth.Login", + "edge": "missing_field", + "input": { + "username": "admin" + }, + "expected": { + "status": 422, + "error_code": "VALIDATION_ERROR", + "error_detail": "Field 'password' is required" + } +} +``` + +**Generation rules:** +- **One JSON file per fixture** — named `_.json` +- **Minimum 5 per C3+ contract**: valid, missing_field, invalid_type, external_fail, rejected_path +- **Expected values ALWAYS hardcoded** — never derived from implementation (anti-tautology) +- **Input values are concrete** — real strings, numbers, objects, not pseudocode +- **Fixture ID format**: `FX_.` — hierarchical, matches contract hierarchy +- **@TEST_FIXTURE in manifest** points to the JSON file path +- **@RELATION VERIFIES** links fixture to production contract +- For `@REJECTED` paths: expected MUST include error/failure, proving the path is unreachable +- For model invariants: input = state before action, expected = state after action +- Do NOT generate executable test files here — only canonical JSON fixtures + +### Fixture Traceability + +Extend `traceability.md` with a Fixture column: + +| Story | Model | Fixture | Task | Test | +|-------|-------|---------|------|------| +| US1 | Api.Auth.Login | FX_Auth.Login.Valid | T017 | Test.Api.Auth + ### Quickstart Output Generate `quickstart.md` using real repository verification paths: @@ -239,6 +336,41 @@ Generate `quickstart.md` using real repository verification paths: - Frontend lint: `cd frontend && npm run lint` - Docker: `docker compose up --build` +### Traceability Matrix Output + +If UX contracts exist (`contracts/ux/` was generated by `/speckit.ux`), generate `traceability.md` — a requirements traceability matrix (RTM) mapping every user story through its implementation chain: + +```markdown +#region Traceability [C:3] [TYPE ADR] [SEMANTICS traceability,rtm,[DOMAIN]] +@defgroup Trace Matrix Requirements → Model → API → Task → Test for [FEATURE]. + +## Traceability Matrix + +| Story | Screen | Model | Fixture | API Endpoint | Backend Task | Frontend Task | Test | +|-------|--------|-------|---------|-------------|-------------|--------------|------| +| US1: [Title] | /route | Domain.Model | FX_Domain.Valid | GET /api/... | T017 | T015 | Test.Domain | +| US1: [Title] | /route | Domain.Model | FX_Domain.MissingField | POST /api/... | T018 | T019 | Test.Domain.Edge | + +## Impact Analysis Quick Reference + +| If you change... | These fixtures verify it | These tests verify it | These screens depend | +|-----------------|------------------------|----------------------|---------------------| +| `GET /api/dashboards` | FX_Dashboards.Hub.* | Test.Dashboards.Hub | /dashboards, /migration | +| `Dashboards.Hub` model | FX_Dashboards.EnvReset | Test.Dashboards.Hub | /dashboards | + +#endregion Traceability +``` + +**Generation rules:** +- One row per unique (Story, API Endpoint, Screen) tuple +- Model column: `[TYPE Model]` contract ID from `screen-models.md` +- API column: endpoint from `api-ux.md` or `contracts/modules.md` +- Task columns: task IDs from `tasks.md` (to be filled after `/speckit.tasks` — leave as `T???` if tasks not yet generated) +- Test column: test contract ID pattern `Test..` +- Impact table: derived from `@RELATION` edges in contracts — invert the dependency graph +- Grep-friendly: `grep "Dashboards.Hub" traceability.md` → all rows for that model +- Agent zombie mode: without MCP tools, `grep "" traceability.md` replaces `impact_analysis` + ## Key Rules - Use absolute paths in workflow execution. diff --git a/.opencode/command/speckit.tasks.md b/.opencode/command/speckit.tasks.md index 7085adff..e51bfa7a 100644 --- a/.opencode/command/speckit.tasks.md +++ b/.opencode/command/speckit.tasks.md @@ -173,3 +173,29 @@ Before finalizing `tasks.md`, verify that: - no task text schedules a rejected path - story tasks remain executable within the actual Python/Svelte project structure - at least one explicit verification task protects against rejected-path regression + +### Fixture Materialization Tasks + +If `/speckit.plan` generated canonical fixtures in `specs//fixtures/`, create materialization tasks that copy them into the repo-native test directories before writing test code: + +**Backend fixtures:** +```text +- [ ] TXXX [P] [US1] Materialize fixtures from specs//fixtures/api/ into backend/tests/fixtures// + Source: fixtures/api/auth_login_*.json + Target: backend/tests/fixtures/auth/ + Each fixture → one JSON file. Do NOT modify fixture content — copy as-is. +``` + +**Frontend fixtures:** +```text +- [ ] TXXX [P] [US1] Materialize fixtures from specs//fixtures/model/ into frontend/src/lib/models/__fixtures__// + Source: fixtures/model/migration_*.json + Target: frontend/src/lib/models/__fixtures__/migration/ +``` + +**Rules:** +- Materialization tasks are [P] (parallel, different directories) +- Materialize BEFORE test-writing tasks — tests import fixtures +- Fixtures are copied as-is from canonical source — no adaptation at this stage +- If canonical fixture shape doesn't match test framework expectations, create a separate adapter task +- Every fixture in `manifest.md` gets exactly one materialization task diff --git a/.opencode/command/speckit.ux.md b/.opencode/command/speckit.ux.md new file mode 100644 index 00000000..491a3897 --- /dev/null +++ b/.opencode/command/speckit.ux.md @@ -0,0 +1,366 @@ +--- +description: Interactive UX design session — asks questions, presents alternatives, exhaustively designs every screen state, then generates Screen Model code and UX contracts. +handoffs: + - label: Build Technical Plan + agent: speckit.plan + prompt: Create a Python/Svelte implementation plan using the UX contracts + send: true + - label: Create Tasks + agent: speckit.tasks + prompt: Break the plan into executable tasks referencing UX contracts +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Principle + +You are a UX designer, not a contract generator. Your job is to **ask questions the spec didn't answer**, present **visual and interaction alternatives**, and work through **every screen state exhaustively** before writing a single contract. Contracts are the OUTPUT of design decisions, not the input. + +## Outline + +### Phase 0: Load Context + +1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` → `FEATURE_DIR`. +2. **Load**: + - `FEATURE_DIR/spec.md` — user stories, acceptance criteria + - `FEATURE_DIR/ux_reference.md` — high-level narrative (if exists) + - `.opencode/skills/semantics-svelte/SKILL.md` — §VI canonical template, §VII design tokens + - `frontend/src/lib/ui/` — available atoms (Button, Card, Input, Select, PageHeader...) + - `frontend/src/lib/components/` — available widgets (MultiSelect, SearchableMultiSelect...) + - `frontend/src/lib/models/` — existing Screen Models (reuse or extend) + +### Phase 1: Screen Decomposition — ASK, don't assume + +For EACH user story in `spec.md` that has a UI surface, ask: + +``` +## Screen: [Story Title] + +**1. Navigation structure** +How does the user reach this screen? +A) Separate route: /feature-name +B) Modal/drawer over existing page +C) Tab/section within existing page: /existing#feature +D) Other: [describe] + +**2. Layout strategy** +A) Single column, full width — simple CRUD +B) Two-column: list + detail panel +C) Wizard: multi-step with progress indicator +D) Dashboard: cards/grid with filters +E) Other: [describe] + +**3. Data density** +How much data does the user see at once? +A) Few items (<20): simple list, no pagination +B) Medium (20-200): paginated table with search +C) Large (200+): paginated table + filters + search +D) Real-time stream: WebSocket updates, auto-scroll +``` + +Present 2-3 concrete alternatives with tradeoffs. Wait for user response before continuing to the next question. + +### Phase 2: State Exhaustion — EVERY screen state + +For each screen, work through ALL states exhaustively. This is where most UX bugs hide — the states between "loading" and "loaded". + +``` +## States for: [Screen] + +For each state, define: Visual → ARIA → User can... + +**Happy path:** +- **idle** → [what user sees before any action] +- **loading** → skeleton? spinner? progress bar? partial data? +- **loaded** → data visible, actions available + +**Empty states:** +- **empty (first use)** → guided onboarding or empty state with CTA? +- **empty (filtered)** → "No results match" + clear filters? +- **empty (no permissions)** → 403 with explanation? + +**Error states:** +- **error (network)** → toast + retry? full error page? degraded mode? +- **error (validation)** → inline field errors? modal? which fields? +- **error (timeout)** → retry with countdown? cancel? +- **error (server 500)** → generic message? retry? contact support? + +**Edge states:** +- **stale data** → show cached with "refresh" indicator? +- **partial data** → some rows loaded, some failed? +- **background update** → data changed by another user? WebSocket notification? +- **rate limited** → "Too many requests" + countdown? +``` + +For EACH state, ask: "Is this state possible? If yes, what does the user see?" + +### Phase 3: Interaction Design — choices with tradeoffs + +For each user action, present alternatives: + +``` +## Interaction: [Action Name] + +**1. Trigger** +A) Button (primary, visible immediately) +B) Button in toolbar (secondary, contextual) +C) Inline action (icon per row, hover reveal) +D) Keyboard shortcut (power users) +E) Context menu (right-click) + +**2. Feedback** +A) Optimistic update (UI changes before API confirms) +B) Loading state on element (button spinner, row skeleton) +C) Full page overlay (block all interactions) +D) Background (toast on completion) + +**3. Confirmation** +A) No confirmation (action is safe/undoable) +B) `confirm()` dialog (simple yes/no) +C) Custom modal (shows affected items, requires explicit confirm) +D) Undo toast (action executes, toast offers undo for 5s) + +**4. Multi-select** +If user can act on multiple items: +A) Checkbox per row + bulk action bar +B) Shift-click range selection +C) Select-all + deselect individually +``` + +Present the tradeoff for each alternative — don't just list options. E.g.: "Optimistic update feels faster but requires rollback logic on failure. Loading spinner is simpler but adds perceived latency." + +### Phase 4: API UX Design + +For each endpoint this feature touches: + +``` +## API: [METHOD] /api/[endpoint] + +**Request:** +- Shape: { field: Type, ... } +- Validation errors → HTTP 422, inline per-field messages + +**Response shapes — ALL variants:** +- Success (200/201): { data: {...}, meta?: {...} } +- Empty (200): { data: [], meta: { total: 0 } } +- Not found (404): { error: { code: "NOT_FOUND", detail: "..." } } +- Permission denied (403): { error: { code: "FORBIDDEN", detail: "..." } } +- Validation (422): { error: { code: "VALIDATION", fields: { field: "message" } } } +- Conflict (409): { error: { code: "CONFLICT", detail: "..." } } +- Server error (500): { error: { code: "INTERNAL", detail: "..." } } + +**Loading UX:** +- Debounce before showing loader? (ms) +- Skeleton or spinner? +- Partial data during load or blank? + +**Sequence (Mermaid — for complex multi-step flows):** +```mermaid +sequenceDiagram + User->>+Frontend: Click "[Action]" + Frontend->>+Backend: POST /api/... + Backend->>+External: [call] + External-->>-Backend: [response] + Backend-->>-Frontend: { status: "ok", data: {...} } + Frontend->>User: [feedback] +``` +Use ONLY for flows with 3+ participants or async callbacks. Skip for simple CRUD. + +**WebSocket (if applicable):** +- Channel: task.{id}.progress +- Payload shape +- How does UI react to each message type? +``` + +### Phase 5: Mobile & Accessibility + +``` +**Mobile behavior:** +- Responsive breakpoint strategy? +- Stacked layout on mobile? Which columns collapse? +- Touch targets: minimum 44×44px per WCAG + +**Accessibility:** +- Screen reader flow for each state +- Focus management: where does focus go after modal opens/closes? +- Keyboard navigation: Tab order, Enter/Space for actions +- Color contrast: semantic tokens guarantee WCAG AA? Check destructive/success on surface. +``` + +### Phase 6: Record Decisions & Alternatives + +After all questions are answered, create TWO artifacts: + +**`contracts/ux/alternatives.md`** — all options considered, BEFORE final choice: + +```markdown +#region UxAlternatives [C:3] [TYPE ADR] [SEMANTICS ux,alternatives,[DOMAIN]] +@defgroup Ux Design alternatives explored for [FEATURE]. + +## Screen: [Name] + +### Navigation +- ✅ CHOSEN: Separate route /feature — clean URL, direct linkable, full focus +- ❌ Rejected: Modal over dashboard — loses context when modal closes, can't deep-link +- ❌ Rejected: Tab within settings — buried, users won't discover + +### Layout +- ✅ CHOSEN: Two-column (list + detail) — best scanability for 20+ items +- ❌ Rejected: Single table — no preview without navigation, repetitive clicks +- ❌ Rejected: Cards grid — doesn't scale past 12 items, inconsistent card heights + +### Data Loading +- ✅ CHOSEN: Paginated table (20 per page) + search — predictable, fast +- ❌ Rejected: Infinite scroll — breaks "select all", hard to find specific item +- ❌ Rejected: Load all at once — 200+ items freeze UI + +### Action Feedback (for destructive actions) +- ✅ CHOSEN: Undo toast (5s) — feels instant, recoverable +- ❌ Rejected: Confirm dialog — extra click on every action, annoying at scale +- ❌ Rejected: No confirmation — dangerous for delete/migrate + +#endregion UxAlternatives +``` + +**`contracts/ux/decisions.md`** — only the final choices: + +```markdown +#region UxDecisions [C:3] [TYPE ADR] [SEMANTICS ux,decisions,[DOMAIN]] +@defgroup Ux Final UX design decisions for [FEATURE]. + +## Screen: [Name] +- Navigation: Separate route /feature +- Layout: Two-column (list + detail) +- Data: Paginated (20/page) + search +- Feedback: Undo toast (5s) for destructive actions + +#endregion UxDecisions +``` + +**Rule:** `alternatives.md` shows the DESIGN SPACE — agent can see WHY each path was rejected. `decisions.md` is the compact reference for `/speckit.plan`. + +### Phase 7: Generate Artifacts + +ONLY after all design decisions are made: + +1. **`contracts/ux/screen-models.md`** — Model inventory from Phase 1-2 decisions +2. **`contracts/ux/api-ux.md`** — API shapes from Phase 4 +3. **`contracts/ux/-ux.md`** × N — per-screen UX contracts from Phase 2-3 +4. **`contracts/ux/design-tokens.md`** — token application from Phase 3 +5. **`frontend/src/lib/models/Model.svelte.ts`** — generated model code + +For artifacts 3-5, use the templates defined below. Do NOT generate contracts before design decisions are recorded. + +### Phase 8: Confirmation Gate + +Before writing model files to `frontend/src/lib/models/`, present: + +| File | Path | Atoms | Actions | Dependencies | +|------|------|-------|---------|-------------| + +Ask: "Write these model files? (yes/no)" + +## Artifact Templates + +### `-ux.md` + +```markdown +#region Ux [C:3] [TYPE ADR] [SEMANTICS ux,,] +@defgroup Ux UX contract for . + +## FSM (from Phase 2 decisions) +idle → [trigger] → loading → [success] → loaded + → [empty] → empty + → [failure] → error → [retry] → loading + +## State Mappings (from Phase 2-3 decisions) +| @UX_STATE | Visual | ARIA | User Can | +|-----------|--------|------|----------| + +## Feedback (from Phase 3 decisions) +| Trigger | Feedback | Rationale | + +## Recovery (from Phase 2 edge states) +| From | Action | To | + +## Reactivity (from Phase 1-2 decisions) +- Model atoms → Component props → DOM +- Store subscriptions → $effect (browser-side only) + +## UX Tests (minimum: happy, empty, error, edge) +| @UX_TEST | Given | When | Then | +``` + +### `Model.svelte.ts` — generated code + +```typescript +// frontend/src/lib/models/Model.svelte.ts +// #region .Model [C:4] [TYPE Model] [SEMANTICS ,,screen-model] +// @defgroup . +// @INVARIANT +// @STATE +// @ACTION +// @RELATION DEPENDS_ON -> [api] +// @RATIONALE Model-first: extracted to enable L1 testing without DOM. +// @REJECTED Inline state rejected — scatters logic across event handlers. + +import { requestApi } from "$lib/api"; +import { log } from "$lib/cot-logger"; + +// ── Types (from Phase 2-4 decisions) ── +type ScreenState = "idle" | "loading" | "loaded" | "empty" | "error"; +interface Entity { id: string; /* from spec + API shape */ } +interface ListResponse { data: Entity[]; meta: { total: number }; } + +export class Model { + // ── Atoms ── + items: Entity[] = $state([]); + screenState: ScreenState = $state("idle"); + error: string | null = $state(null); + + // ── Derived ── + isEmpty = $derived(this.items.length === 0 && this.screenState === "loaded"); + + // ── Actions ── + async load(): Promise { + this.screenState = "loading"; + this.error = null; + log(".Model", "REASON", "Loading items"); + try { + const res: ListResponse = await requestApi("/api/..."); + this.items = res.data; + this.screenState = this.items.length === 0 ? "empty" : "loaded"; + log(".Model", "REFLECT", "Items loaded", { count: this.items.length }); + } catch (e: unknown) { + this.error = e instanceof Error ? e.message : "Load failed"; + this.screenState = "error"; + log(".Model", "EXPLORE", "Load failed", {}, this.error); + } + } + + async retry(): Promise { await this.load(); } + + // TODO: implement remaining actions from Phase 3 decisions + // Each action throws until implemented — L1-testable immediately +} +// #endregion .Model +``` + +## Stop & Report + +After Phase 8, report: +- Screens designed: N +- Design decisions recorded: N +- UX contracts generated: N files +- Model files generated: N (if confirmed) +- Total @UX_STATE mappings: N +- Total @UX_TEST scenarios: N +- Every screen state from Phase 2 covered: yes/no +- Every API response variant from Phase 4 covered: yes/no +- Readiness for `/speckit.plan` diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 43c13525..405b7271 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -67,11 +67,12 @@ All generated contracts (specs, code, tests) MUST be optimized for the attention ## Development Workflow ```text -/speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.implement +/speckit.specify → /speckit.clarify → /speckit.ux → /speckit.plan → /speckit.tasks → /speckit.implement ``` **Rules:** - No phase is skipped when `[NEEDS CLARIFICATION]` markers remain +- `/speckit.ux` produces UX contracts AND generates Screen Model `.svelte.ts` files — these are the design contract for `/speckit.plan` - Contract mutation: preview → apply, never immediate apply - All feature artifacts in `specs//`, never in `.kilo/` or `.ai/` diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index baa1a7fd..1b04d6a9 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -44,8 +44,17 @@ specs/[###-feature]/ ├── research.md # Phase 0 output (/speckit.plan command) ├── data-model.md # Phase 1 output (/speckit.plan command) ├── quickstart.md # Phase 1 output (/speckit.plan command) +├── traceability.md # Phase 1 output — RTM: Story → Model → API → Task → Test ├── contracts/ # Phase 1 output (/speckit.plan command) -└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +│ ├── modules.md # Module & function contracts +│ └── ux/ # UX contracts (if /speckit.ux was run) +│ ├── alternatives.md # Design space explored +│ ├── decisions.md # Final UX choices +│ ├── screen-models.md # Model inventory +│ ├── api-ux.md # API interaction shapes +│ ├── -ux.md # Per-screen UX contracts +│ └── design-tokens.md # Applied tokens & reuse +└── tasks.md # Phase 2 output (/speckit.tasks command) ``` ### Source Code (repository root) diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md index c9d911a6..3eabba8f 100644 --- a/.specify/templates/tasks-template.md +++ b/.specify/templates/tasks-template.md @@ -68,6 +68,8 @@ Examples of foundational tasks (adjust based on your project): - [ ] T007 Create base models/entities that all stories depend on - [ ] T008 Configure error handling and logging infrastructure - [ ] T009 Setup environment configuration management +- [ ] T00A [P] Materialize canonical fixtures from specs//fixtures/api/ into backend/tests/fixtures/ +- [ ] T00B [P] Materialize canonical fixtures from specs//fixtures/model/ into frontend/src/lib/models/__fixtures__/ **Checkpoint**: Foundation ready - user story implementation can now begin in parallel @@ -279,3 +281,5 @@ With multiple developers: - **Screen Models use `.svelte.ts` extension** — create them in `frontend/src/lib/models/` - **Two-layer testing: L1 (model invariants, no render, ~27ms) comes BEFORE L2 (UX contracts, with render)** - **Backend tasks are pinned to `backend/src/`, frontend tasks to `frontend/src/`** — cross-stack tasks reference both +- **Fixture materialization tasks come BEFORE test-writing tasks** — tests import fixtures, not generate data +- **Canonical fixtures live in `specs//fixtures/`** — materialize to `tests/` via tasks, never edit canonical source in test directories diff --git a/frontend/src/lib/models/AgentChatModel.svelte.ts b/frontend/src/lib/models/AgentChatModel.svelte.ts new file mode 100644 index 00000000..fa8cc303 --- /dev/null +++ b/frontend/src/lib/models/AgentChatModel.svelte.ts @@ -0,0 +1,325 @@ +// frontend/src/lib/models/AgentChatModel.svelte.ts +// #region AgentChat.Model [C:4] [TYPE Model] [SEMANTICS agent-chat,model,gradio,langgraph] +// @defgroup AgentChat State model for Gradio-powered agent chat — submit() streaming, structured metadata, LangGraph HITL resume, no WebSocket. +// @INVARIANT Streaming state gates input: input disabled in streaming, awaiting_confirmation, disconnected states. +// @INVARIANT Connection state gates send: send rejected when connectionState !== "connected". +// @INVARIANT HITL resume: confirmation via second submit() with additional_inputs=[conversationId, "confirm"|"deny"]. Primary stream TERMINATES on confirm_required, second stream resumes from checkpoint. +// @INVARIANT Optimistic cancel: submission.cancel() stops generation, partial text preserved. +// @INVARIANT Conversation list loaded from FastAPI REST — Gradio serves only live agent streaming. +// @STATE idle | streaming | awaiting_confirmation | error | disconnected | disconnected_permanent +// @ACTION sendMessage(text, files?) — submit("/chat", {text, files}, [conversationId, null]), iterate stream events, process metadata. +// @ACTION cancelGeneration() — submission.cancel(), optimistic UI reset. +// @ACTION resumeConfirm(action) — second submit("/chat", {text:"confirm"}, [conversationId, action]) for HITL resume. +// @ACTION loadConversations(reset?) — fetch list from REST, infinite scroll. +// @ACTION loadHistory(conversationId?) — fetch messages from REST. +// @ACTION deleteConversation(id) — optimistic archive, rollback on failure. +// @ACTION createConversation() — clear state, start new. +// @ACTION retryConnection() — Client.connect() retry. +// @RELATION BINDS_TO -> [AssistantChatStore] +// @RELATION DEPENDS_ON -> [GradioClient:@gradio/client] +// @RELATION DEPENDS_ON -> [AssistantApi] +// @RATIONALE Model-first: extracted from AssistantChatPanel.svelte (1029 lines). Centralizes Gradio submit() lifecycle, metadata parsing, HITL resume, and reconnect logic in a testable unit. Without a model, streaming + metadata dispatch + confirmation + reconnect logic scattered across event handlers would be untestable. +// @REJECTED Inline $state in AssistantChatPanel.svelte — rejected because component already exceeds 400-line guideline. +// @REJECTED WebSocket-based model — rejected with custom WebSocket protocol. +// @REJECTED Active/follower multi-tab — rejected in favor of client-side gate. + +import { Client, type Client as GradioClient } from "@gradio/client"; +import { + assistantChatStore, + setAssistantConversationId, +} from "$lib/stores/assistantChat.svelte.js"; +import { + getAssistantConversations, + getAssistantHistory, + deleteAssistantConversation, +} from "$lib/api/assistant.js"; +import { addToast } from "$lib/toasts.svelte.js"; +import { log } from "$lib/cot-logger"; + +// ── Type definitions ───────────────────────────────────────────── + +type StreamingState = + | "idle" + | "streaming" + | "awaiting_confirmation" + | "error" + | "disconnected" + | "disconnected_permanent"; + +type ConnectionState = + | "connected" + | "disconnected" + | "disconnected_permanent"; + +interface StreamMetadata { + type?: "stream_token" | "tool_start" | "tool_end" | "tool_error" + | "confirm_required" | "confirm_resolved" | "error"; + token?: string; + tool?: string; + input?: Record; + output?: Record; + error?: string; + prompt?: string; + thread_id?: string; + result?: "confirmed" | "denied"; + code?: string; + detail?: string; +} + +interface ToolCall { + tool: string; + input: Record; + output?: Record; + error?: string; + status: "executing" | "completed" | "failed"; +} + +interface AgentMessage { + id: string; + conversation_id: string; + role: "user" | "assistant" | "system" | "tool"; + text: string; + metadata?: StreamMetadata; + state?: string; + task_id?: string; + toolCalls: ToolCall[]; + created_at: string; +} + +interface Conversation { + id: string; + title: string; + updated_at: string; + message_count: number; +} + +// ── Reconnect constants ────────────────────────────────────────── + +const RECONNECT_MAX_ATTEMPTS = 5; +const RECONNECT_INTERVAL_MS = 5_000; + +export class AgentChatModel { + // ── Atoms ────────────────────────────────────────────────────── + messages: AgentMessage[] = $state([]); + conversations: Conversation[] = $state([]); + currentConversationId: string | null = $state(null); + streamingState: StreamingState = $state("idle"); + connectionState: ConnectionState = $state("connected"); + error: string | null = $state(null); + partialText: string = $state(""); + activeToolCalls: ToolCall[] = $state([]); + isLoadingHistory: boolean = $state(false); + inputText: string = $state(""); + pendingThreadId: string | null = $state(null); // for HITL resume + + // ── Private atoms ────────────────────────────────────────────── + private _client: GradioClient | null = null; + private _submission: ReturnType | null = null; + private _conversationsPage: number = 1; + private _conversationsHasNext: boolean = $state(false); + private _historyPage: number = 1; + private _historyHasNext: boolean = $state(false); + private _reconnectAttempts: number = 0; + private _reconnectTimer: ReturnType | null = null; + + // ── Derived ──────────────────────────────────────────────────── + isStreaming = $derived( + this.streamingState === "streaming" || + this.streamingState === "awaiting_confirmation", + ); + isInputLocked = $derived( + this.isStreaming || + this.streamingState === "disconnected" || + this.streamingState === "disconnected_permanent", + ); + connectionDotColor = $derived( + this.connectionState === "connected" ? "success" + : this.connectionState === "disconnected" ? "warning" + : "destructive", + ); + + // ── Actions — P1 ─────────────────────────────────────────────── + + async sendMessage(text: string, files?: File[]): Promise { + if (this.streamingState !== "idle" || this.connectionState !== "connected") return; + if (!text.trim() && (!files || files.length === 0)) return; + + const convId = this.currentConversationId; + this.streamingState = "streaming"; + this.error = null; + this.partialText = ""; + this.activeToolCalls = []; + log("AgentChat.Model", "REASON", "Sending message", { text: text.slice(0, 100) }); + + try { + this._submission = this._client!.submit("/chat", + { text, files }, + [convId, null], // additional_inputs: [conversation_id, action] + ); + + for await (const event of this._submission) { + const msg: AgentMessage = event.data; + this._onStreamData(msg); + } + this.streamingState = "idle"; + this._submission = null; + + } catch (e: unknown) { + this.streamingState = "error"; + this.error = e instanceof Error ? e.message : "Stream failed"; + log("AgentChat.Model", "EXPLORE", "Stream failed", {}, this.error); + } + } + + cancelGeneration(): void { + if (this.streamingState === "idle") return; + this._submission?.cancel(); + this.streamingState = "idle"; + this._submission = null; + log("AgentChat.Model", "REASON", "Generation cancelled"); + } + + async resumeConfirm(action: "confirm" | "deny"): Promise { + if (this.streamingState !== "awaiting_confirmation") return; + if (!this.pendingThreadId || !this.currentConversationId) return; + + log("AgentChat.Model", "REASON", `HITL resume: ${action}`, { threadId: this.pendingThreadId }); + + try { + const submission = this._client!.submit("/chat", + { text: action === "confirm" ? "confirm" : "deny" }, + [this.currentConversationId, action], + ); + + for await (const event of submission) { + const msg: AgentMessage = event.data; + this._onStreamData(msg); + } + this.streamingState = "idle"; + this.pendingThreadId = null; + + } catch (e: unknown) { + this.streamingState = "error"; + this.error = e instanceof Error ? e.message : "Resume failed"; + log("AgentChat.Model", "EXPLORE", "HITL resume failed", {}, this.error); + } + } + + async loadConversations(reset: boolean = false): Promise { + log("AgentChat.Model", "REASON", "Loading conversations", { reset }); + throw new Error("TODO: implement loadConversations — GET /api/assistant/conversations, infinite scroll, merge or replace list"); + } + + async loadHistory(conversationId: string | null = null): Promise { + this.isLoadingHistory = true; + this.error = null; + log("AgentChat.Model", "REASON", "Loading history", { conversationId }); + throw new Error("TODO: implement loadHistory — GET /api/assistant/history, render messages"); + } + + async retryConnection(): Promise { + log("AgentChat.Model", "REASON", "Manual reconnect"); + this._reconnectAttempts = 0; + throw new Error("TODO: implement retryConnection — Client.connect() to Gradio, reset reconnect counter"); + } + + createConversation(): void { + this.messages = []; + this.currentConversationId = null; + this.streamingState = "idle"; + this.error = null; + this.partialText = ""; + this.activeToolCalls = []; + this.pendingThreadId = null; + this._historyPage = 1; + this._historyHasNext = false; + log("AgentChat.Model", "REASON", "New conversation created"); + } + + // ── Actions — P2 ─────────────────────────────────────────────── + + async deleteConversation(id: string): Promise { + this.conversations = this.conversations.filter((c) => c.id !== id); + log("AgentChat.Model", "REASON", "Archiving conversation (optimistic)", { id }); + throw new Error("TODO: implement deleteConversation — DELETE /api/assistant/conversations/{id}, soft-delete (archive), rollback on failure"); + } + + // ── Private — Stream metadata handler ────────────────────────── + + private _onStreamData(msg: AgentMessage): void { + const meta = msg.metadata; + if (!meta || !meta.type) { + // Plain text token — append to last assistant message + this.partialText += msg.text; + return; + } + + switch (meta.type) { + case "stream_token": + this.partialText += meta.token ?? ""; + break; + + case "tool_start": + this.activeToolCalls = [...this.activeToolCalls, { + tool: meta.tool ?? "unknown", + input: meta.input ?? {}, + status: "executing", + }]; + break; + + case "tool_end": + this.activeToolCalls = this.activeToolCalls.map((tc) => + tc.tool === meta.tool + ? { ...tc, output: meta.output, status: "completed" } + : tc + ); + break; + + case "tool_error": + this.activeToolCalls = this.activeToolCalls.map((tc) => + tc.tool === meta.tool + ? { ...tc, error: meta.error, status: "failed" } + : tc + ); + break; + + case "confirm_required": + this.streamingState = "awaiting_confirmation"; + this.pendingThreadId = meta.thread_id ?? null; + // Primary stream terminates — model enters awaiting_confirmation + // Second submit() will call resumeConfirm() + break; + + case "confirm_resolved": + this.streamingState = meta.result === "confirmed" ? "streaming" : "idle"; + this.pendingThreadId = null; + break; + + case "error": + this.streamingState = "error"; + this.error = meta.detail ?? meta.code ?? "Unknown error"; + break; + + default: + log("AgentChat.Model", "EXPLORE", "Unknown metadata type", { type: meta.type }); + } + } + + // ── Private — Connection lifecycle ───────────────────────────── + + private _onDisconnect(): void { + this.connectionState = "disconnected"; + this._reconnectAttempts = 0; + log("AgentChat.Model", "EXPLORE", "Gradio disconnected"); + throw new Error("TODO: implement _onDisconnect — start reconnect interval (5×5s), transition to connected or disconnected_permanent"); + } + + private _onReconnect(): void { + this.connectionState = "connected"; + this.streamingState = "idle"; + this._reconnectAttempts = 0; + log("AgentChat.Model", "REFLECT", "Gradio reconnected"); + throw new Error("TODO: implement _onReconnect — clear timer, restore connected state"); + } +} +// #endregion AgentChat.Model diff --git a/merge_spec.py b/merge_spec.py index 90d2da86..c2e2f749 100644 --- a/merge_spec.py +++ b/merge_spec.py @@ -9,22 +9,48 @@ from datetime import datetime from pathlib import Path REVIEW_PROMPT = ( - "Другая LLM создала этот feature-пакет. Твоя задача - провести независимое " - "ортогональное spec review, оценить готовность спецификации, найти противоречия, " - "пробелы, риски реализации и подготовить структурированный отчет с корректировками. " - "Сфокусируйся именно на review пакета спецификации, а не на переписывании реализации." + "Another LLM created this feature package. Your task: conduct an independent " + "orthogonal spec review. Evaluate readiness, find contradictions, gaps, " + "implementation risks, and prepare a structured report with corrections. " + "Focus on spec review, not rewriting the implementation." ) +# Canonical artifact order for the merged output. +# Each stage: (match_type, value, section_label) +# match_type "exact" = exact filename match +# match_type "prefix" = directory prefix — matches all files under that dir CANONICAL_MD_STAGES = ( - ("exact", "spec.md"), - ("exact", "ux_reference.md"), - ("prefix", "checklists/"), - ("exact", "plan.md"), - ("exact", "research.md"), - ("exact", "data-model.md"), - ("prefix", "contracts/"), - ("exact", "quickstart.md"), - ("exact", "tasks.md"), + # Layer 1: Requirements & UX narrative + ("exact", "spec.md", "SPEC — Feature Specification"), + ("exact", "ux_reference.md", "UX REFERENCE — Interaction Narrative"), + ("prefix", "checklists/", "CHECKLISTS — Requirements Quality"), + + # Layer 2: UX Design (from /speckit.ux) + ("exact", "contracts/ux/alternatives.md", "UX ALTERNATIVES — Design Space Explored"), + ("exact", "contracts/ux/decisions.md", "UX DECISIONS — Final Choices"), + ("exact", "contracts/ux/screen-models.md", "UX SCREEN MODELS — Model Inventory"), + ("exact", "contracts/ux/api-ux.md", "UX API CONTRACT — Endpoints & Shapes"), + ("prefix", "contracts/ux/", "UX DESIGN — Per-Screen Contracts"), + ("exact", "contracts/ux/design-tokens.md", None), # force-first within prefix + + # Layer 3: Implementation Plan + ("exact", "plan.md", "PLAN — Implementation Plan"), + ("exact", "research.md", "RESEARCH — Technical Decisions"), + ("exact", "data-model.md", "DATA MODEL — Entities & Relations"), + ("prefix", "contracts/", "CONTRACTS — Module & Function Contracts"), + ("exact", "quickstart.md", "QUICKSTART — Dev Onboarding"), + + # Layer 4: Traceability + ("exact", "traceability.md", "TRACEABILITY — Requirements Matrix"), + + # Layer 5: Execution + ("exact", "tasks.md", "TASKS — Implementation Tasks"), +) + +ORDER_HINT = ( + "Artifact order: spec → ux_reference → checklists → " + "UX (alternatives → decisions → screen-models → api-ux → per-screen → design-tokens) → " + "plan → research → data-model → contracts → quickstart → traceability → tasks → remaining." ) @@ -33,44 +59,60 @@ def relative_key(path: Path, root: Path) -> str: def ordered_markdown_files(target_dir: Path) -> list[Path]: + """Order markdown files according to CANONICAL_MD_STAGES.""" markdown_files = [path for path in target_dir.rglob("*.md") if path.is_file()] remaining = {relative_key(path, target_dir): path for path in markdown_files} ordered: list[Path] = [] + # Track which stages have already been matched to avoid duplicates + seen = set() - for stage_type, stage_value in CANONICAL_MD_STAGES: + for stage_type, stage_value, _section_label in CANONICAL_MD_STAGES: if stage_type == "exact": - path = remaining.pop(stage_value, None) - if path is not None: + # Exact: only match if the file exists and hasn't been collected yet + rel = f"contracts/ux/{stage_value}" if stage_value.startswith("contracts/ux/") else stage_value + if stage_value.startswith("contracts/ux/"): + path = remaining.pop(stage_value, None) + else: + path = remaining.pop(stage_value, None) + if path is not None and path not in seen: ordered.append(path) + seen.add(path) continue + # Prefix: match all files whose relative path starts with stage_value stage_matches = sorted( [ path - for relative_path, path in remaining.items() - if relative_path.startswith(stage_value) + for rel_path, path in remaining.items() + if rel_path.startswith(stage_value) ], - key=lambda path: relative_key(path, target_dir), + key=lambda p: relative_key(p, target_dir), ) - ordered.extend(stage_matches) for path in stage_matches: - remaining.pop(relative_key(path, target_dir), None) + if path not in seen: + ordered.append(path) + seen.add(path) + rel = relative_key(path, target_dir) + remaining.pop(rel, None) - ordered.extend( - sorted(remaining.values(), key=lambda path: relative_key(path, target_dir)) - ) + # Remaining files — append at end + remaining_paths = sorted(remaining.values(), key=lambda p: relative_key(p, target_dir)) + for path in remaining_paths: + if path not in seen: + ordered.append(path) return ordered -def merge_specs(feature_number): +def merge_specs(feature_number: str) -> str | None: + """Merge all spec artifacts for a feature into one review file.""" specs_dir = Path("specs") if not specs_dir.exists(): print("Error: 'specs' directory not found.") - return + return None - # Find the directory starting with the feature number + # Find directory starting with feature number target_dir = None for item in specs_dir.iterdir(): if item.is_dir() and item.name.startswith(f"{feature_number}-"): @@ -78,10 +120,8 @@ def merge_specs(feature_number): break if not target_dir: - print( - f"Error: No directory found for feature number '{feature_number}' in 'specs/'." - ) - return + print(f"Error: No directory found for feature number '{feature_number}' in 'specs/'.") + return None feature_name = target_dir.name now = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -90,7 +130,11 @@ def merge_specs(feature_number): content_blocks = [ REVIEW_PROMPT, "", - "Порядок артефактов: spec -> ux_reference -> checklist -> plan -> research -> data-model -> contracts -> quickstart -> tasks -> remaining markdown.", + "=" * 80, + ORDER_HINT, + f"Feature: {feature_name}", + f"Generated: {datetime.now().isoformat()}", + "=" * 80, "", ] @@ -99,19 +143,44 @@ def merge_specs(feature_number): for file_path in files_to_merge: relative_path = file_path.relative_to(target_dir) try: - with open(file_path, "r", encoding="utf-8") as f: - file_content = f.read() - - content_blocks.append(f"--- FILE: {relative_path} ---\n") - content_blocks.append(file_content) - content_blocks.append("\n") + content = file_path.read_text(encoding="utf-8") except Exception as e: - print(f"Skipping file {file_path} due to error: {e}") + print(f"Skipping {file_path}: {e}") + continue - with open(output_filename, "w", encoding="utf-8") as f: - f.write("\n".join(content_blocks)) + # Determine section label + section_label = str(relative_path) + for stage_type, stage_value, label in CANONICAL_MD_STAGES: + if label is None: + continue + if stage_type == "exact" and str(relative_path) == stage_value: + section_label = label + break + if stage_type == "prefix" and str(relative_path).startswith(stage_value): + section_label = f"{label} — {relative_path.name}" + break - print(f"Successfully created: {output_filename}") + content_blocks.append("") + content_blocks.append("-" * 60) + content_blocks.append(f"## {section_label}") + content_blocks.append(f"Source: {relative_path}") + content_blocks.append("-" * 60) + content_blocks.append("") + content_blocks.append(content) + content_blocks.append("") + + # Summary + content_blocks.append("") + content_blocks.append("=" * 80) + content_blocks.append(f"Files merged: {len(files_to_merge)}") + content_blocks.append("=" * 80) + + merged = "\n".join(content_blocks) + output_filename = output_filename + Path(output_filename).write_text(merged, encoding="utf-8") + + print(f"Successfully created: {output_filename} ({len(files_to_merge)} files)") + return output_filename if __name__ == "__main__": @@ -120,6 +189,7 @@ if __name__ == "__main__": sys.exit(1) merge_specs(sys.argv[1]) + # #endregion merge_spec # #endregion MergeSpec diff --git a/specs/033-gradio-agent-chat/checklists/requirements.md b/specs/033-gradio-agent-chat/checklists/requirements.md new file mode 100644 index 00000000..ae909e32 --- /dev/null +++ b/specs/033-gradio-agent-chat/checklists/requirements.md @@ -0,0 +1,85 @@ +# Requirements Quality Checklist: Gradio Agent Chat + +**Purpose**: Validate spec.md against the constitution, ADRs, and ss-tools repository reality. Surface gaps before `/speckit.clarify` or `/speckit.plan`. +**Created**: 2026-06-08 +**Feature**: `specs/033-gradio-agent-chat/spec.md` + +## 1. No Implementation Leakage + +- [x] CHK001 Spec describes WHAT the user needs — not Python module names, file paths, or Svelte component internals. +- [x] CHK002 No Pydantic schema names, SQLAlchemy table names, or internal function signatures appear in spec. +- [x] CHK003 Architecture diagram is conceptual (shows systems, not classes) — appropriate for spec level. +- [x] CHK004 Functional requirements use domain language: "agent", "tool", "streaming", "conversation" — not "WebSocket handler", "SSE endpoint", "gr.ChatInterface". + +## 2. ss-tools Stack Compatibility + +- [x] CHK005 Gradio integration reuses existing `@assistant_tool` registry (ADR-0008) — no duplicate tool registration. +- [x] CHK006 Svelte frontend remains the primary application shell (ADR-0006: Svelte 5 runes mandate). +- [x] CHK007 `@gradio/client` npm package is the bridge between Svelte and Python Gradio — documented and feasible. +- [x] CHK008 Authentication model reuses existing JWT tokens (ADR-0005: RBAC). +- [x] CHK009 Gradio backend runs within same Docker network — no new external dependencies exposed publicly. +- [x] CHK010 Existing assistant REST endpoints (`/api/assistant/*`) preserved during transition (FR-020). + +## 3. Measurable Success Criteria + +- [x] CHK011 SC-001: Tool selection accuracy ≥85% — measurable via acceptance test suite. +- [x] CHK012 SC-002: First token latency ≤1.5s for 90% of queries — measurable via client-side timing. +- [x] CHK013 SC-003: Dangerous ops confirmation rate 100% — measurable via audit log verification. +- [x] CHK014 SC-004: Permission bypass rate 0% — measurable via security test suite. +- [x] CHK015 SC-005: Conversation persistence across restart — measurable via integration test. +- [x] CHK016 SC-006: Speed improvement ≥30% vs manual UI — measurable via usability benchmark. +- [x] CHK017 SC-007: File upload ≤10MB without responsiveness degradation — measurable via load test. +- [x] CHK018 SC-008: Existing UX patterns preserved — measurable via regression test suite. + +## 4. Edge Cases & Recovery + +- [x] CHK019 Gradio backend unreachable → connection error with retry (FR-014). +- [x] CHK020 Malformed LLM tool-call JSON → retry + clarification fallback (Edge Cases). +- [x] CHK021 Message exceeds token limit → truncation + warning (Edge Cases). +- [x] CHK022 Tool execution timeout → async progress indicator (FR-012, Edge Cases). +- [x] CHK023 Rapid-fire messages → sequential queue with position display (Edge Cases). +- [x] CHK024 Gradio restart mid-conversation → persisted history survives, streaming session lost (Edge Cases). +- [x] CHK025 Confirmation token expiry → inform user, offer re-issue (Edge Cases). +- [x] CHK026 Multiple browser tabs with same conversation → reject concurrent writes (Edge Cases). +- [x] CHK027 Network loss during streaming → detect disconnection, offer reconnect (Edge Cases). + +## 5. Decision-Memory Readiness + +- [x] CHK028 `@RATIONALE` in spec documents why Gradio was chosen over custom SSE/WebSocket implementation. +- [x] CHK029 `@REJECTED` documents why full Svelte replacement was rejected (ADR-0006 violation). +- [x] CHK030 `@REJECTED` documents why building streaming from scratch was rejected. +- [x] CHK031 Phase 1 research.md will need to resolve: Gradio `gr.ChatInterface` vs `gr.Blocks` + `gr.Chatbot` choice. +- [x] CHK032 Phase 1 research.md will need to resolve: `@gradio/client` streaming event API surface for Svelte 5 runes integration. +- [x] CHK033 Phase 1 research.md will need to resolve: Agent framework choice (LangChain agent executor vs custom loop vs smolagents). + +## 6. Completeness + +- [x] CHK034 All 6 user stories are independently testable (each has an "Independent Test" sentence). +- [x] CHK035 P1 stories cover the MVP: streaming chat + autonomous tool use. +- [x] CHK036 P2 stories cover UX quality: tool visibility + conversation context. +- [x] CHK037 P3 stories cover polish: persistence + file upload. +- [x] CHK038 Key entities defined: AgentConversation, AgentMessage, AgentToolCall, AgentStreamingSession, AgentMemory. +- [x] CHK039 20 functional requirements cover: streaming, tool use, RBAC, persistence, UX preservation, backward compatibility. +- [x] CHK040 Dependencies section names specific existing artifacts to integrate with. + +## 7. Constitution Alignment + +- [x] CHK041 Principle I (Semantic Contract First): Spec uses `#region`/`#endregion` anchors with complexity tiers. +- [x] CHK042 Principle II (Decision Memory): `@RATIONALE` and `@REJECTED` annotations present for architectural choices. +- [x] CHK043 Principle III (External Orchestrator): Gradio backend calls existing APIs — does not embed in Superset. +- [x] CHK044 Principle IV (Module Discipline): New Gradio code placed in clear module structure (planning phase will define). +- [x] CHK045 Principle V (RBAC): FR-005, FR-006, SC-004 enforce existing RBAC on all agent tool calls. +- [x] CHK046 Principle VI (Svelte 5 Runes): Frontend changes use Svelte 5 runes; Gradio is backend-only. +- [x] CHK047 Principle VIII (Attention-Optimized): Spec uses hierarchical IDs (`AgentChat.Spec`), one-line anchors with @SEMANTICS tags, ATTN_2 domain prefixes. + +## 8. Unresolved Markers + +- [x] CHK048 No `[NEEDS CLARIFICATION]` markers remain in spec.md. All ambiguities resolved with informed defaults. +- [x] CHK049 No `[NEED_CONTEXT: ...]` markers — all dependencies identified in the Dependencies section. + +## Notes + +- **Agent framework choice** (LangChain vs smolagents vs custom) is deferred to research.md in Phase 1 — this is an implementation detail, not a spec ambiguity. +- **Gradio auth bridge** (how JWT is forwarded to Gradio) needs resolution in planning phase — `@gradio/client` options include auth headers; exact mechanism TBD. +- **Gradio backend deployment** (separate container vs co-located with FastAPI) is an infrastructure decision for plan.md, not spec.md. +- All checklist items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`. diff --git a/specs/033-gradio-agent-chat/contracts/modules.md b/specs/033-gradio-agent-chat/contracts/modules.md new file mode 100644 index 00000000..945e7ca6 --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/modules.md @@ -0,0 +1,30 @@ +#region AgentChat.Contracts [C:4] [TYPE ADR] [SEMANTICS contracts,modules,agent-chat,final] +@BRIEF Final module contracts — LangChain v1, PostgreSQL checkpointer, structured JSON metadata, dual-identity RBAC. + +## Backend + +| Contract | C | File | Notes | +|----------|---|------|-------| +| AgentChat.GradioApp | C4 | `backend/src/agent/app.py` | `gr.ChatInterface(type="messages", multimodal=True, additional_inputs=[gr.Textbox("conversation_id")], examples=[["Покажи дашборды",null],["Статус системы",null]], concurrency_limit=1)`. Handler: `def handler(message, history, request: gr.Request, conversation_id=None)` | +| AgentChat.GradioApp.Handler | C4 | ↑ | Creates `create_react_agent()` with PostgresSaver. `astream_events(config={"configurable":{"thread_id":conversation_id}})`. Resume: detects `"confirm"/"deny"` in additional_inputs, invokes `Command(resume=...)` | +| AgentChat.LangChain.Setup | C4 | `backend/src/agent/langchain_setup.py` | `create_agent(model, tools=ALL_TOOLS, middleware=[LoggingMiddleware(), HumanInTheLoopMiddleware(interrupt_on=DANGEROUS)], checkpointer=PostgresSaver(conn_string))`. Wrapped in `RunnableWithMessageHistory` | +| AgentChat.Tools | C4 | `backend/src/agent/tools.py` | Native `@tool` functions. Each: Pydantic `BaseModel` args_schema, calls FastAPI REST with dual-identity headers | +| AgentChat.Middleware | C3 | `backend/src/agent/middleware.py` | `LoggingMiddleware`, risk config dict | +| AgentChat.Document.Parser | C3 | `backend/src/agent/document_parser.py` | pdfplumber + openpyxl | +| AgentChat.Api.Conversations | C3 | `backend/src/api/routes/agent_conversations.py` | CRUD + multi-tab gate | +| AgentChat.Api.ServiceToken | C3 | `backend/src/api/routes/auth.py` | `POST /api/auth/service-token` | +| Models.Agent | C2 | `backend/src/models/agent.py` | AgentConversation, AgentMessage | +| Schemas.Agent | C1 | `backend/src/schemas/agent.py` | Pydantic | + +## Frontend + +| Contract | C | File | Notes | +|----------|---|------|-------| +| AgentChat.Model | C4 | `AgentChatModel.svelte.ts` | `submit()` lifecycle, metadata parsing, resume logic. No tabRole, no follower | +| AgentChat.Panel | C4 | `AssistantChatPanel.svelte` | `Client.connect("/api/agent/gradio")`, `submit()`, metadata-based rendering | +| AgentChat.Page | C3 | `frontend/src/routes/agent/+page.svelte` | Two-column | +| AgentChat.ConversationList | C3 | `ConversationList.svelte` | Search, infinite scroll, date grouping | +| AgentChat.ToolCallCard | C2 | `ToolCallCard.svelte` | Props from metadata | +| AgentChat.ConfirmationCard | C3 | `ConfirmationCard.svelte` | Calls second `submit()` with `__resume__` | + +#endregion AgentChat.Contracts diff --git a/specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md b/specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md new file mode 100644 index 00000000..433ff5f9 --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md @@ -0,0 +1,88 @@ +#region AgentChat.Ux [C:3] [TYPE ADR] [SEMANTICS ux,agent-chat,panel,native-stack] +@defgroup Ux UX contract — Gradio submit() + LangChain v1 astream_events(). Structured JSON metadata (ChatMessage.metadata). No REST, no WebSocket, no follower. + +## FSM + +``` +idle ──[submit("/chat")]──→ streaming ──[metadata.type: stream_token]──→ (tokens appear) + streaming ──[metadata.type: tool_start]──→ (tool card + spinner) + streaming ──[metadata.type: tool_end]──→ (tool done, checkmark) + streaming ──[metadata.type: tool_error]──→ (tool error, cross) + streaming ──[metadata.type: confirm_required]──→ awaiting_confirmation + awaiting_confirmation ──[2nd submit(__resume__,"confirm")]──→ streaming (confirm_resolved) + awaiting_confirmation ──[2nd submit(__resume__,"deny")]──→ idle (confirm_resolved: denied) + awaiting_confirmation ──[timeout 5min]──→ idle (expired) + streaming ──[final chunk]──→ idle + streaming ──[submission.cancel()]──→ idle (partial) + streaming ──[metadata.type: error]──→ error + +idle ──[load history]──→ loading_history ──[success]──→ idle + loading_history ──[failure]──→ error + +idle ──[connect fail]──→ disconnected ──[retry 5×5s]──→ idle + disconnected ──[retry exhausted]──→ disconnected_permanent +``` + +## State Mappings + +| @UX_STATE | Visual | ARIA | User Can | +|-----------|--------|------|----------| +| idle (with msgs) | Input enabled, Send active, Stop hidden. Connection dot green. | — | Send, switch/delete convos, expand to /agent | +| idle (new convo) | Welcome card + examples chips (Gradio `examples`). | `aria-label="Новый диалог"` | Click chip, type | +| streaming | Tokens appear in real time. Tool cards inline. Send→Stop (destructive). Input locked. | `aria-live="polite" aria-busy="true"` | Stop, expand tool cards | +| awaiting_confirmation | Streaming pauses. Warning card: `bg-warning-light border-warning-DEFAULT`. Prompt text + Confirm/Deny buttons. Timer 5:00→0:00. | `role="alertdialog"` | Confirm, Deny | +| awaiting_confirmation (expired) | Card stays. Buttons disabled. Timer=0. "Повторить запрос?" | — | Retry | +| error | Card: `bg-destructive-light border-destructive-ring`. Error + Retry. Partial text preserved. | `role="alert"` | Retry, new message | +| disconnected | Dot red. Banner: "Агент недоступен. Переподключение через Xс..." | `role="alert"` | Read history, manual reconnect | +| disconnected_permanent | Dot red. Manual reconnect button only. | `role="alert"` | Manual reconnect | +| loading_history | Skeleton cards (3-5, `animate-pulse bg-surface-muted`). Input locked. | `aria-busy="true"` | Wait | + +## Feedback + +| Trigger | Feedback | +|---------|----------| +| `metadata.type: stream_token` | Append token to assistant message | +| `metadata.type: tool_start` | Tool card: name (mono) + spinner | +| `metadata.type: tool_end` | Spinner→checkmark, result in tooltip | +| `metadata.type: tool_error` | Spinner→cross, error in tooltip | +| `metadata.type: confirm_required` | Confirmation card + timer, Confirm/Deny | +| `metadata.type: confirm_resolved(result=confirmed)` | Card collapses, streaming resumes | +| `metadata.type: confirm_resolved(result=denied)` | Card collapses, agent responds | +| Stream complete | Stop→Send, input enabled | +| Connection lost | Dot red. Banner. Retry countdown | +| Connection restored | Dot green. Banner dismissed | + +## Recovery + +| From | Action | To | +|------|--------|-----| +| error | Retry (re-sends last msg) | streaming | +| disconnected | Auto-retry 5×5s or manual | idle | +| awaiting_confirmation (expired) | "Повторить запрос" | streaming | +| streaming (cancelled) | Type new msg | idle→streaming | +| empty (no convos) | Click chip or type | streaming | + +## Reactivity + +- `AgentChat.Model` atoms → component props → DOM +- `@gradio/client` `submit()` → `for await (event)` → `model._onStreamData()` → `$state` update +- No `$effect` for data loading — all via stream events +- `assistantChatStore.conversationId` → model reads (read-only) + +## UX Tests + +| @UX_TEST | Given | When | Then | +|----------|-------|------|------| +| Happy path | idle (new) | Type "привет", Send | streaming, tokens appear, idle | +| Tool call | streaming | Agent calls tool | Card with spinner→checkmark | +| Dangerous confirm | streaming | Agent wants deploy | "⏸️" card, confirm_id, timer | +| Confirm accepted | awaiting_confirmation | 2nd submit(__resume__) | "▶️", streaming resumes | +| Confirm denied | awaiting_confirmation | 2nd submit(deny) | "⏹️", idle | +| Stream cancel | streaming | submission.cancel() | idle, partial text | +| LLM error | streaming | LLM fails | "Ошибка: ..." + retry | +| Gradio disconnect | idle | Gradio dies | Dot red, 5×5s retry | +| Multi-tab gate | idle | Tab 2 opens same convo | Tab 2: send rejected | +| Empty convo | idle (new) | Load | Examples chips visible | +| History load error | idle | API fails | Error + retry | + +#endregion AgentChat.Ux diff --git a/specs/033-gradio-agent-chat/contracts/ux/api-ux.md b/specs/033-gradio-agent-chat/contracts/ux/api-ux.md new file mode 100644 index 00000000..41ac6e43 --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/ux/api-ux.md @@ -0,0 +1,90 @@ +#region AgentChat.ApiUx [C:3] [TYPE ADR] [SEMANTICS ux,api,agent-chat,final] +@defgroup Ux Final API contract — Gradio `submit()`, LangChain v1 HITL via second submit(), PostgreSQL checkpointer, structured metadata. + +## Gradio Transport + +**Connection**: `Client.connect("/api/agent/gradio")` → nginx proxy → Gradio. JWT forwarded. + +**Primary submit**: `app.submit("/chat", {message, conversation_id})` — two-arg API. `conversation_id` is an `additional_inputs` key. +**Examples on UI** (for `additional_inputs`): `[["Покажи дашборды", null], ["Статус системы", null]]`. + +**Cancel**: `submission.cancel()`. + +### Streaming Events + +`for await (event of submission)` — `event.data` is a `ChatMessage` with structured `metadata`: + +| `metadata.type` | `metadata` fields | UX Reaction | +|-----------------|-------------------|-------------| +| `stream_token` | `{token: "При"}` | Append token to last assistant msg | +| `tool_start` | `{tool: "search_dashboards", input: {...}}` | Tool card: name + spinner | +| `tool_end` | `{tool: "search_dashboards", output: {...}}` | Spinner→checkmark | +| `tool_error` | `{tool: "search_dashboards", error: "..."}` | Spinner→cross + detail | +| `confirm_required` | `{prompt: "Подтвердить деплой?", thread_id: "..."}` | Confirmation card + timer | +| `confirm_resolved` | `{result: "confirmed"/"denied"}` | Card collapses | +| `error` | `{code: "LLM_UNAVAILABLE", detail: "..."}` | Error card + retry | +| (none) | plain `{content: "..."}` | Normal text | + +### HITL Resume Protocol + +``` +Primary: submit("/chat", {message, conversation_id}) + → handler: agent.astream_events(config={"thread_id": conversation_id}) + → HITL fires → checkpoint saved (PostgreSQL) + → handler: yield {metadata: {type: "confirm_required", thread_id: conversation_id}} + → Svelte: renders confirmation card (stream still open) + +Confirm: submit("/chat", message, [conversation_id, "confirm"]) + → handler: detects "confirm" in additional_inputs[1] + → loads checkpoint by conversation_id as thread_id + → invokes graph with Command(resume={"action": "confirm"}, config={"thread_id": conversation_id}) + +Deny: submit("/chat", message, [conversation_id, "deny"]) + → handler: Command(resume={"action": "deny"}, config=...) +``` + +--- + +## REST Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `POST` | `/api/assistant/conversations` | Create → `{id, title, created_at}` | +| `GET` | `/api/assistant/conversations` | Paginated list + search | +| `GET` | `/api/assistant/history` | Paginated messages | +| `POST` | `/api/assistant/conversations/{id}/messages` | Append after stream_end | +| `DELETE` | `/api/assistant/conversations/{id}` | Soft-delete (is_archived=true) | +| `GET` | `/api/agent/conversations/{id}/active` | Multi-tab gate | +| `POST` | `/api/auth/service-token` | Gradio obtains service JWT | + +--- + +## Dual Identity RBAC + +``` +Gradio handler: + service_jwt = POST /api/auth/service-token (authenticates the agent service) + user_jwt = request.headers["Authorization"] (browser user) + +@tool function: + call FastAPI with: + Authorization: Bearer {service_jwt} (agent is authorized to call) + X-User-JWT: {user_jwt} (operation authorized under user) + +FastAPI endpoint: + validates service JWT → agent is legitimate + validates X-User-JWT → user has RBAC permission for this operation + audit: {service_actor: "agent", user_actor: user_id, operation, ...} +``` + +## Error Taxonomy + +| Code | Source | UX | +|------|--------|-----| +| `UNAUTHORIZED` | JWT | Redirect /login | +| `FORBIDDEN` | RBAC | metadata.type="error" in stream | +| `LLM_UNAVAILABLE` | LangChain | metadata.type="error" → card + retry | +| `GRADIO_UNREACHABLE` | connect | Dot red, retry 5×5s | +| `TOOL_FAILURE` | FastAPI | metadata.type="tool_error" | + +#endregion AgentChat.ApiUx diff --git a/specs/033-gradio-agent-chat/contracts/ux/decisions.md b/specs/033-gradio-agent-chat/contracts/ux/decisions.md new file mode 100644 index 00000000..edc287aa --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/ux/decisions.md @@ -0,0 +1,48 @@ +#region AgentChat.UxDecisions [C:3] [TYPE ADR] [SEMANTICS ux,decisions,agent-chat,native-stack] +@defgroup Ux UX design decisions — revised for LangChain v1 native stack + Gradio native features. Replaces prior decisions.md. + +## Screen: Agent Chat Panel + +### Phase 1 — Screen Decomposition + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Navigation** | C — Гибрид: drawer 420px + кнопка «Развернуть» → `/agent` | Быстрые вопросы в drawer; сложные на `/agent` | +| **Layout** | B — Две колонки на `/agent`, одно колоночное в drawer | Структурированно для `/agent`, drawer компактный | +| **Data density** | B — Infinite scroll для сообщений и диалогов + поиск | Для активного оператора с 50+ диалогами | + +### Phase 2 — State Exhaustion + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Чипсы примеров** | A — Только в новом пустом диалоге | Welcome-зона. Gradio `examples` рендерит их нативно | +| **Cancel** | A — Единая кнопка Stop с момента отправки. `submission.cancel()` | Нативный Gradio cancel | +| **Confirmation timeout** | A — Карточка остаётся, кнопки заблокированы, «Повторить» | Явный контроль | +| **Reconnect** | A — 5×5s, затем disconnected_permanent + ручная кнопка | Предсказуемо | +| **Ошибка истории** | A — Сразу ошибка, без кэша | Консистентность | +| **Multi-tab** | A — Client-side gate: `GET /api/agent/conversations/{id}/active` перед отправкой. Отклоняет вторую вкладку | Просто, без server-push. `concurrency_limit=1` на Gradio исключает гонки | + +### Phase 3 — Interaction Design + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Отправка** | Gradio `submit()` с `additional_inputs=[conversation_id]` | Нативный механизм. `RunnableWithMessageHistory` загружает историю автоматически | +| **Stop** | `submission.cancel()` — optimistic UI | Нативный Gradio cancel. Частичный текст сохранён | +| **Подтверждение** | Second `submit()` с `__resume__` + `__action__`. In-memory pending interrupts в handler | Два submit() к одному стриму. HumanInTheLoopMiddleware + чекпоинт | + +### Phase 4 — API Design + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Транспорт** | Gradio `submit()` через nginx proxy. Structured JSON metadata (`ChatMessage.metadata.type`) | Нулевой custom код. LangGraph события → структурированные объекты | +| **Диалоги REST** | Плоский список с сервера, группировка на клиенте | API не зависит от визуализации | + +## Rejected Alternatives + +- **Custom WebSocket protocol** — rejected: @gradio/client не raw WebSocket +- **REST confirmation endpoints** — rejected: HumanInTheLoopMiddleware + resume protocol проще +- **Ручная передача history** — rejected: RunnableWithMessageHistory делает автоматически +- **Active/follower multi-tab** — rejected: client-side gate + concurrency_limit=1 достаточно +- **@assistant_tool registry для новых тулов** — rejected: нативные @tool функции LangChain + +#endregion AgentChat.UxDecisions diff --git a/specs/033-gradio-agent-chat/contracts/ux/design-tokens.md b/specs/033-gradio-agent-chat/contracts/ux/design-tokens.md new file mode 100644 index 00000000..89d9d5ae --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/ux/design-tokens.md @@ -0,0 +1,113 @@ +#region AgentChat.DesignTokens [C:2] [TYPE ADR] [SEMANTICS ux,tokens,agent-chat] +@defgroup Ux Design tokens and component reuse for Gradio Agent Chat (drawer + /agent page). + +## Component Reuse Scan + +| Need | Existing Asset | Action | +|------|---------------|--------| +| Primary button (send, confirm, retry) | `$lib/ui/Button.svelte` variant="primary" | Reuse | +| Secondary/ghost (cancel, delete) | `$lib/ui/Button.svelte` variant="ghost" | Reuse | +| Destructive (stop, delete confirm) | `$lib/ui/Button.svelte` variant="destructive" | Reuse | +| Empty state (new convo, no history) | `$lib/ui/EmptyState.svelte` | Reuse | +| Page header (/agent page) | `$lib/ui/PageHeader.svelte` | Reuse | +| Icons (send, stop, paperclip, expand) | `$lib/ui/Icon.svelte` | Reuse — name="send"/"stop"/"paperclip"/"chevron-down" | +| Toast notifications | `addToast()` from `$lib/toasts` | Reuse | +| Confirmation (convo delete) | `confirm()` (native browser) | Pattern | +| Custom confirmation (in-chat) | New `ConfirmationCard` inline within message | New component | +| Tool-call card | New `ToolCallCard` inline within message | New component | + +--- + +## Semantic Tokens — Drawer (420px) + +### Chat Panel Shell +| Element | Token | +|---------|-------| +| Drawer background | `bg-surface-page` | +| Drawer left border | `border-l border-border` | +| Header (convo switcher + expand btn) | `bg-surface-card border-b border-border` | +| Message scroll area | `bg-surface-page` | +| Input area | `border-t border-border bg-surface-card` | + +### Messages +| Element | Token | +|---------|-------| +| User bubble | `bg-primary text-white rounded-2xl` | +| Assistant bubble | `bg-surface-card border border-border rounded-2xl` | +| Streaming cursor | `animate-pulse text-primary` (blinking block, 0.3s) | +| System/info message | `bg-surface-muted text-text-muted text-sm rounded-lg` | + +### Tool-Call Cards +| Element | Token | +|---------|-------| +| Card | `bg-surface-muted border border-border rounded-lg px-3 py-2` | +| Tool name | `text-text-muted text-xs font-mono` | +| Spinner (executing) | `animate-spin text-primary` | +| Checkmark (done) | `text-success` | +| Cross (error) | `text-destructive` | +| Expanded content | `text-text-muted text-xs font-mono overflow-auto max-h-32` | + +### Confirmation Cards +| Element | Token | +|---------|-------| +| Card | `bg-warning-light border border-warning-DEFAULT rounded-lg p-3` | +| Warning icon | `text-warning` | +| Op summary | `text-text font-medium` | +| Timer | `text-text-muted text-xs` | + +### Connection Status +| Element | Token | +|---------|-------| +| Connected (green) | `bg-success w-2 h-2 rounded-full` | +| Reconnecting (yellow) | `bg-warning w-2 h-2 rounded-full animate-pulse` | +| Disconnected (red) | `bg-destructive w-2 h-2 rounded-full` | + +### Input Area +| Element | Token | +|---------|-------| +| Textarea | `border border-border-strong rounded-xl focus:border-primary focus:ring-1 focus:ring-primary-ring` | +| Send button | `bg-primary text-white rounded-full` | +| Stop button | `bg-destructive text-white rounded-full` | + +--- + +## Semantic Tokens — `/agent` Page + +### Page Layout +| Element | Token | +|---------|-------| +| Page background | `bg-surface-page` | +| Page shell | `max-w-7xl mx-auto px-4 py-6 h-[calc(100vh-4rem)]` | +| Two-column container | `flex gap-0 h-full` | + +### Conversation Sidebar (240px) +| Element | Token | +|---------|-------| +| Sidebar | `bg-surface-card border-r border-border w-60 shrink-0` | +| Search input | `border border-border-strong rounded-lg focus:border-primary` | +| Active convo highlight | `bg-surface-muted` | +| Convo title | `text-text text-sm font-medium truncate` | +| Convo date group header | `text-text-muted text-xs font-semibold uppercase` | +| Convo date | `text-text-muted text-xs` | +| Delete button (hover) | `text-text-muted hover:text-destructive` | + +### Main Chat Area +| Element | Token | +|---------|-------| +| Chat container | `flex-1 flex flex-col` | +| Messages area | `flex-1 overflow-y-auto bg-surface-page` | +| Input area | `border-t border-border bg-surface-card p-4` | + +### Welcome / Empty +| Element | Token | +|---------|-------| +| Welcome card (new convo) | `bg-surface-card border border-border rounded-xl p-6 max-w-md mx-auto` | +| Chip (example) | `border border-border rounded-full px-3 py-1.5 text-sm text-text-muted hover:bg-surface-muted hover:text-text cursor-pointer transition` | + +--- + +## No Raw Tailwind Colors + +All tokens reference `tailwind.config.js` semantic palette. Zero `blue-*`, `gray-*`, `red-*`, `green-*`, `amber-*`, `slate-*`, `indigo-*` in page/component code. Verification: `scripts/audit-frontend-style.mjs`. + +#endregion AgentChat.DesignTokens diff --git a/specs/033-gradio-agent-chat/contracts/ux/screen-models.md b/specs/033-gradio-agent-chat/contracts/ux/screen-models.md new file mode 100644 index 00000000..24a532eb --- /dev/null +++ b/specs/033-gradio-agent-chat/contracts/ux/screen-models.md @@ -0,0 +1,29 @@ +#region AgentChat.ScreenModels [C:3] [TYPE ADR] [SEMANTICS ux,screen-models,agent-chat] +@BRIEF Screen model inventory — revised for LangChain v1 native stack. No active/follower. No WebSocket. +@RATIONALE Model-first per ADR-0010. AgentChatModel handles Gradio submit() lifecycle, streaming state, tool-call tracking, multi-tab gate. + +## Screens Requiring Models + +| Screen | Route | Model ID | Complexity | Rationale | +|--------|-------|----------|------------|-----------| +| AssistantChatPanel (drawer + embedded) | overlay | `AgentChat.Model` | C4 | Gradio `submit()` lifecycle, streaming, tool-call parsing, confirmation card state, connection health | +| Agent Chat Page (full) | `/agent` | `AgentChat.Model` (shared) | C4 | Same model, two-column layout | + +## Screens Using Inline State + +| Screen | Reason | +|--------|--------| +| ConversationList | `conversations[]` из модели. Поиск — локальный. Группировка — `$derived` | +| ToolCallCard | Пропсы от родителя | +| ConfirmationCard | Пропсы от модели (`confirm_id`, `prompt`) | +| MessageBubble | Рендерит одно сообщение. Стриминг — через `$derived` | +| ConnectionIndicator | `model.connectionState` → dot color | + +## Model Decomposition Gate + +| Threshold | Est. | Action | +|-----------|:----:|--------| +| Lines > 400 | ~220 | Within limit | +| Methods > 40 | ~12 | Within limit | + +#endregion AgentChat.ScreenModels diff --git a/specs/033-gradio-agent-chat/data-model.md b/specs/033-gradio-agent-chat/data-model.md new file mode 100644 index 00000000..b701ec97 --- /dev/null +++ b/specs/033-gradio-agent-chat/data-model.md @@ -0,0 +1,52 @@ +#region AgentChat.DataModel [C:3] [TYPE ADR] [SEMANTICS data-model,agent-chat,final] +@BRIEF Final data model — structured JSON metadata, PostgreSQL checkpointer, dual-identity RBAC, no custom WebSocket types. + +## 1. Streaming Metadata Protocol + +Each `event.data` is a `ChatMessage` with optional structured `metadata`: + +```typescript +interface StreamMetadata { + type?: "stream_token" | "tool_start" | "tool_end" | "tool_error" + | "confirm_required" | "confirm_resolved" | "error"; + token?: string; + tool?: string; + input?: Record; + output?: Record; + error?: string; + prompt?: string; + thread_id?: string; + result?: "confirmed" | "denied"; + code?: string; + detail?: string; +} +``` + +No `agent-ws.ts`. No custom WebSocket types. No emoji prefix parsing. + +## 2. SQLAlchemy + +Tables: `agent_conversations`, `agent_messages`. Legacy `assistant_messages` preserved. + +## 3. Pydantic Schemas + +`ConversationItem`, `ConversationListResponse`, `MessageItem`, `HistoryResponse`, `DeleteResponse`. + +## 4. TypeScript DTOs + +`frontend/src/types/agent.ts` — matches Pydantic schemas. Includes `StreamMetadata`. + +## 5. PostgreSQL Checkpointer + +LangGraph checkpointer: `langgraph-checkpoint-postgres`. Thread ID = conversation_id. Survives container restarts. Same PostgreSQL instance as FastAPI. + +## 6. Dual Identity RBAC + +``` +Authorization: Bearer {service_jwt} +X-User-JWT: {user_jwt} +``` + +Audit: `{service_actor: "agent", user_actor: user_id}`. + +#endregion AgentChat.DataModel diff --git a/specs/033-gradio-agent-chat/plan.md b/specs/033-gradio-agent-chat/plan.md new file mode 100644 index 00000000..fbab1b2c --- /dev/null +++ b/specs/033-gradio-agent-chat/plan.md @@ -0,0 +1,71 @@ +# Implementation Plan: Gradio Agent Chat (LangChain v1 Native Stack) + +**Branch**: `033-gradio-agent-chat` | **Date**: 2026-06-08 | **Spec**: `spec.md` + +## Summary + +Integrate Gradio as agent backend, Svelte frontend via `@gradio/client submit()`. LangGraph agent: `create_react_agent()` + `@tool` + `interrupt()`/`Command(resume=...)` + `RunnableWithMessageHistory` + `PostgresSaver`. Structured JSON metadata streaming. No REST confirmations, no custom WebSocket. + +## Technical Context + +**Language**: Python 3.9+/TypeScript Svelte 5 runes +**Key Dependencies**: Gradio≥5.0, LangChain≥1.0, langchain-openai≥1.0, langgraph-checkpoint, pdfplumber, openpyxl (back); @gradio/client (front) +**Storage**: PostgreSQL 16 (persistence + checkpoints via langgraph-checkpoint-postgres) +**Testing**: pytest (back), vitest L1 model + L2 UX (front) +**Frontend**: SvelteKit SPA, model-first (AgentChatModel.svelte.ts C4), runes-only +**Target**: Linux/Docker, modern browsers +**Performance**: First token <1.5s, streaming 60fps, file upload ≤10MB + +## Constitution Check — All 8 Principles Passed + +| Principle | Compliance | +|-----------|-----------| +| I. Semantic Contract First | All 18 contracts use #region/#endregion, hierarchical IDs, C1-C4 tiers | +| II. Decision Memory | @RATIONALE/@REJECTED on all 8 research decisions; T030 deprecates @assistant_tool | +| III. External Orchestrator | Gradio agent calls FastAPI services; no Superset plugin coupling | +| IV. Module Discipline | All modules <400 lines; model ~220 lines | +| V. RBAC | User JWT forwarded to each tool call (closure factory); permission checks in FastAPI | +| VI. Svelte 5 Runes | $state/$derived only; no legacy syntax | +| VII. Test-Driven C3+ | @TEST_EDGE on all C3+ contracts; 10 test tasks | +| VIII. Attention-Optimized | ATTN_1-4 validated; one-line anchors, hierarchical IDs, shared @SEMANTICS, ≤150 lines | + +## Project Structure + +### New/Changed Files + +``` +backend/src/agent/ +├── app.py # gr.ChatInterface + handler + resume protocol +├── langchain_setup.py # create_agent() + middleware + checkpointer +├── tools.py # @tool functions → FastAPI REST +├── middleware.py # LoggingMiddleware, risk config +├── document_parser.py # PDF + XLSX parsing +└── run.py # entrypoint + +backend/src/api/routes/ +├── agent_conversations.py # CRUD + multi-tab gate +└── auth.py # POST /api/auth/service-token + +backend/src/models/agent.py # AgentConversation, AgentMessage +backend/src/schemas/agent.py # Pydantic schemas + +frontend/src/lib/models/AgentChatModel.svelte.ts # submit() lifecycle +frontend/src/lib/components/assistant/ +├── AssistantChatPanel.svelte # adapted for @gradio/client submit() +├── ConversationList.svelte # new +├── ToolCallCard.svelte # new +├── ConfirmationCard.svelte # new +└── ConnectionIndicator.svelte # new +frontend/src/routes/agent/+page.svelte # new +frontend/src/types/agent.ts # TypeScript DTOs + +docker/Dockerfile.agent # new +docker/docker-compose.yml # updated +docker/nginx.conf # updated: proxy /api/agent/gradio +``` + +## Deprecated + +`backend/src/api/routes/assistant/_tool_registry.py` + `_tool_*.py` → `@DEPRECATED` Tombstone. Old `@assistant_tool` registry preserved for FR-020. + +#endregion (plan.md) diff --git a/specs/033-gradio-agent-chat/quickstart.md b/specs/033-gradio-agent-chat/quickstart.md new file mode 100644 index 00000000..f7e128dc --- /dev/null +++ b/specs/033-gradio-agent-chat/quickstart.md @@ -0,0 +1,57 @@ +#region AgentChat.Quickstart [C:2] [TYPE ADR] [SEMANTICS quickstart,agent-chat,langchain-v1] +@BRIEF Developer quickstart — Gradio + LangChain v1 native stack. + +## Quick Start (Docker) + +```bash +docker compose build ss-tools-agent +docker compose up -d +# Frontend: http://localhost:5173 → login → assistant toggle +# Gradio proxy: http://localhost:8000/api/agent/gradio +``` + +## Quick Start (Local) + +```bash +# Terminal 1: FastAPI +cd backend && source .venv/bin/activate +uvicorn src.app:app --reload --port 8000 + +# Terminal 2: Gradio Agent +cd backend && source .venv/bin/activate +pip install gradio langchain langchain-openai langgraph-checkpoint pdfplumber +python -m src.agent.run # Port 7860 + +# Terminal 3: Svelte +cd frontend && npm install @gradio/client && npm run dev -- --port 5173 +``` + +## Verification + +```bash +# Backend tests +cd backend && source .venv/bin/activate && python -m pytest tests/ -v + +# Frontend tests +cd frontend && npm run test + +# Lint +cd backend && python -m ruff check src/agent/ +cd frontend && npm run lint + +# Gradio health +curl http://localhost:7860/health +``` + +## Environment + +```bash +LLM_API_KEY=sk-... +LLM_BASE_URL=https://api.openai.com/v1 +LLM_MODEL=gpt-4o +JWT_SECRET= +FASTAPI_URL=http://ss-tools-api:8000 +SERVICE_TOKEN_SECRET= +``` + +#endregion AgentChat.Quickstart diff --git a/specs/033-gradio-agent-chat/research.md b/specs/033-gradio-agent-chat/research.md new file mode 100644 index 00000000..83a9149c --- /dev/null +++ b/specs/033-gradio-agent-chat/research.md @@ -0,0 +1,52 @@ +#region AgentChat.Research [C:3] [TYPE ADR] [SEMANTICS research,agent-chat,langgraph,gradio] +@BRIEF Research — LangGraph agent with interrupt/resume, PostgresSaver, structured metadata. Final architecture choices. + +## 1. Agent Framework: LangGraph `create_react_agent()` + +**Decision**: `create_react_agent(model, tools, checkpointer=PostgresSaver)` from `langgraph.prebuilt`. Uses `interrupt()` for HITL, `Command(resume=...)` for resume. +**Rationale**: Native HITL support, PostgresSaver checkpointer, `astream_events()` for structured streaming. +**Rejected**: LangChain AgentExecutor — no native interrupt/checkpointer, more boilerplate. + +## 2. Tools: Native `@tool` + +**Decision**: `@tool`-decorated functions with Pydantic `args_schema`. Each tool calls FastAPI REST via HTTP with user JWT. +**Rationale**: Single source of metadata. Auto OpenAI function schema. Old `@assistant_tool` → `@DEPRECATED`. +**Rejected**: Dual registration (`@assistant_tool` + `StructuredTool`) — redundant. + +## 3. Confirmation: `HumanInTheLoopMiddleware` + +**Decision**: Second `submit()` with `__resume__` protocol. In-memory pending interrupts in handler. +**Rationale**: Zero REST endpoints. LangChain checkpoint ensures safe resume. +**Rejected**: REST confirmation endpoints + polling — more code, more latency. + +## 4. History: `RunnableWithMessageHistory` + +**Decision**: LangChain auto-loads/saves message history. Svelte does NOT pass `history` — only `conversation_id`. +**Rationale**: Eliminates manual history management on frontend and backend. +**Rejected**: Manual `history` parameter in `submit()` — fragile. + +## 5. Checkpoints: `PostgresSaver` + +**Decision**: PostgreSQL via `langgraph-checkpoint-postgres`. Same instance as FastAPI. Survives all container restarts. Thread ID = conversation_id. +**Rationale**: Agent state persists across container restarts. +**Rejected**: In-memory only — lost on restart. + +## 6. Gradio Native Features + +**Decision**: `additional_inputs`, `gr.Request`, `concurrency_limit=1`, `examples`. All used. +**Rationale**: Eliminates custom code for conversation_id passing, JWT extraction, message queuing, welcome chips. +**Rejected**: Custom headers, manual queues — redundant. + +## 7. Tool Execution: HTTP to FastAPI + +**Decision**: All `@tool` functions call FastAPI REST with user JWT (not service JWT). +**Rationale**: RBAC enforced by FastAPI under user identity. Service JWT only for bootstrap. +**Rejected**: Direct import — Gradio has no DB connection. + +## 8. Streaming: Structured JSON Metadata + +**Decision**: LangGraph events yield `ChatMessage` objects with `metadata.type` (stream_token, tool_start, tool_end, tool_error, confirm_required, confirm_resolved, error). Frontend parses structured JSON from `event.data`, not emoji strings. +**Rationale**: Structured metadata is deterministic, typed, and localizable. Emoji string parsing is brittle and collides with model output. +**Rejected**: Plain-text prefix protocol — rejected because emoji prefix parsing is fragile, hard to localize, and error-prone. + +#endregion AgentChat.Research diff --git a/specs/033-gradio-agent-chat/spec.md b/specs/033-gradio-agent-chat/spec.md new file mode 100644 index 00000000..1e741062 --- /dev/null +++ b/specs/033-gradio-agent-chat/spec.md @@ -0,0 +1,133 @@ +#region AgentChat.Spec [C:3] [TYPE ADR] [SEMANTICS spec,requirements,agent-chat,gradio,langgraph] +@BRIEF Feature specification — Gradio `submit()` + LangGraph agent with `interrupt()`/`Command(resume=...)`. No custom WebSocket, no REST confirmations, no @assistant_tool for new code. +@RATIONALE LangGraph chosen over LangChain AgentExecutor: `interrupt()` + `Command(resume=...)` are native HITL features, `PostgresSaver` is LangGraph-native checkpointer, `create_react_agent` from `langgraph.prebuilt` provides standard tool-calling. Gradio `submit()` for streaming, nginx reverse proxy for browser access. +@REJECTED Replacing Svelte frontend with Gradio UI — violates ADR-0006. +@REJECTED Custom SSE/WebSocket — @gradio/client `submit()` is the native transport. +@REJECTED Custom bidirectional WebSocket — @gradio/client is request/job-oriented, not raw WS. +@REJECTED REST confirmation endpoints — LangGraph `interrupt()` eliminates them. +@REJECTED @assistant_tool for new tools — native LangChain `@tool` is SSOT. Old registry → @DEPRECATED for FR-020. + +**Feature Branch**: `033-gradio-agent-chat` +**Created**: 2026-06-08 | **Status**: Draft | **Last revised**: 2026-06-08 (consistency sweep) + +## Architecture Overview + +``` +Browser (SvelteKit) Docker +┌──────────────────────┐ nginx ┌─────────────────────┐ +│ @gradio/client │──→ /api/agent/gradio─→│ Gradio Agent :7860 │ +│ submit("/chat", │ proxy+JWT │ gr.ChatInterface │ +│ {message}, │ │ create_agent(model, │ +│ conversation_id) │ │ tools=[@tool...], │ +│ for await(event) {} │ │ middleware=[HitL], │ +│ submission.cancel() │ │ checkpointer=PG) │ +│ │ │ @tool→HTTP→FastAPI │ +│ REST /api/assistant/*│──────────────────────→│_____________________│ +└──────────────────────┘ │ HTTP + ┌─────────▼───────────┐ + │ FastAPI :8000 │ + │ PostgreSQL │ + │ /api/assistant (leg.)│ + │ /api/auth/svc-token │ + └─────────────────────┘ +``` + +## User Stories + +### Story 1 — Gradio Agent Engine with Streaming Chat (P1) 🎯 + +**Independent Test**: `Client.connect("/api/agent/gradio")`, `submit("/chat", {message}, conversation_id)`, verify tokens stream via `for await (event)`. + +**Acceptance**: +1. **Given** Gradio running, **When** Svelte calls `submit()`, **Then** tokens stream in real time via `event.data` chunks. +2. **Given** streaming in progress, **When** user calls `submission.cancel()`, **Then** generation stops, partial text preserved. + +### Story 2 — Autonomous Tool Selection (P1) 🎯 + +**Independent Test**: "покажи дашборды с проблемами валидации" → agent calls search + health tools. + +**Acceptance**: +1. **Given** user query, **When** agent processes it, **Then** agent selects and invokes `@tool` functions from `backend/src/agent/tools.py`. +2. **Given** tool requires confirmation, **When** agent calls it, **Then** HumanInTheLoopMiddleware pauses execution, UI shows confirmation card. +3. **Given** tool fails, **When** error returned, **Then** agent explains failure and suggests recovery. + +### Story 3 — Tool-Call Visibility (P2) + +### Story 4 — Multi-Turn Context (P2) — via `RunnableWithMessageHistory` + +### Story 5 — Conversation Persistence (P3) — PostgreSQL SSOT, REST CRUD + +### Story 6 — File Upload & Document Analysis (P2) — multimodal=True, pdfplumber/openpyxl + +### Edge Cases +- Gradio unreachable → reconnect 5×5s +- LLM malformed → retry once, then error +- Message too long → truncate + warn +- Tool >30s → async, result when ready +- Multi-tab → REST gate: `GET /api/agent/conversations/{id}/active` rejects second tab +- Network lost → detect disconnect, offer retry + +## Functional Requirements + +### Transport & Core +- **FR-001**: Gradio `gr.ChatInterface(type="messages", multimodal=True)`. Consumable via `@gradio/client` `submit("/chat", payload)` with `additional_inputs=conversation_id`. +- **FR-002**: Svelte frontend uses `@gradio/client submit()` exclusively. No `predict()`, no custom WebSocket. +- **FR-003**: Streaming via LangGraph `agent.astream_events()` → yield → `event.data` chunks. Frontend iterates `for await (event of submission)`. + +### Agent & Tools +- **FR-004**: LangGraph `create_react_agent(model, tools, checkpointer=PostgresSaver)` from `langgraph.prebuilt`. One-line creation. +- **FR-005**: Native `@tool` functions in `backend/src/agent/tools.py`. Each calls FastAPI REST. Old `@assistant_tool` registry → `@DEPRECATED` Tombstone. +- **FR-006**: `@tool` functions use Pydantic `BaseModel` for `args_schema`. Docstring = description. Single source of metadata. +- **FR-007**: Tools call FastAPI with **dual identity**: service JWT authenticates the agent, user JWT (forwarded from browser via closure) authorizes the operation. RBAC enforced by FastAPI under user identity. + +### Confirmation (HumanInTheLoop) +- **FR-008**: LangGraph `interrupt()` in dangerous tool nodes (`deploy_dashboard`, `execute_migration`, `commit_changes`). Handled entirely by LangGraph — **zero REST confirmation endpoints**. +- **FR-009**: Resume mechanism: graph pauses at `interrupt_before` node → primary `submit()` stream yields `metadata.type="confirm_required"` with `thread_id` and **terminates** (stream ends, does NOT wait). User confirms → second `submit("/chat", msg, [conversation_id, "confirm"])` → handler loads checkpoint by thread_id → invokes graph with `Command(resume={"action":"confirm"}, config={"thread_id":conversation_id})` → graph resumes from checkpoint → new stream begins. + +### Persistence +- **FR-010**: Conversation history in PostgreSQL (`agent_conversations`, `agent_messages`). REST: `POST /api/assistant/conversations`, `GET .../conversations`, `GET .../history`, `DELETE .../{id}`. +- **FR-011**: Soft-delete: `is_archived=true`. Renamed to "Archive" in UX. +- **FR-012**: LangGraph checkpointer uses **PostgreSQL** (same instance as FastAPI) via `langgraph-checkpoint-postgres`. Survives container restarts. Thread ID = conversation_id. +- **FR-013**: `RunnableWithMessageHistory` auto-loads context from PostgreSQL on resume. Svelte does NOT pass `history` — only `conversation_id` via `additional_inputs`. + +### UX & Cancel +- **FR-014**: `submission.cancel()` — native cancel. Partial text preserved. +- **FR-015**: `concurrency_limit=1` on handler. Server-side per-user lock prevents concurrent sends. `GET /api/agent/conversations/{id}/active` as additional gate for multi-tab. +- **FR-016**: Tool-call visibility via **structured JSON metadata in `ChatMessage.metadata`**, not emoji string parsing. `{type: "tool_start", tool: "...", input: {...}}`. +- **FR-017**: Confirmation card renders from `ChatMessage.metadata.confirm_required`. + +### Auth +- **FR-018**: Browser → nginx → Gradio. nginx forwards `Authorization` header. Gradio handler extracts via `request: gr.Request.headers`. +- **FR-019**: Gradio → FastAPI: service JWT from `POST /api/auth/service-token`. Dual identity: service JWT authenticates agent, user JWT authorizes tool calls. +- **FR-020**: Stateless JWT validation in Gradio using shared `JWT_SECRET`. No DB required for auth. + +### File Upload +- **FR-021**: PDF (pdfplumber), XLSX (openpyxl), JSON, CSV, PNG, JPEG. `multimodal=True` handles upload. Parser in `backend/src/agent/document_parser.py`. + +### Backward Compat +- **FR-022**: Existing `/api/assistant` REST preserved. Old `@assistant_tool` → `@DEPRECATED` Tombstone. +- **FR-023**: Existing Svelte UX patterns preserved: drawer/embedded variants, focus-target sync. + +### Additional +- **FR-024**: Audit logging via `LoggingMiddleware` — all interactions to `assistant_audit`. +- **FR-025**: RU/EN i18n keys for all new UI strings. + +## Success Criteria + +- **SC-001**: Tool selection ≥85% accuracy +- **SC-002**: First token <1.5s +- **SC-003**: Dangerous ops 100% confirmation +- **SC-004**: RBAC bypass 0% +- **SC-005**: History survives restart +- **SC-006**: Multi-step ≥30% faster than manual UI +- **SC-007**: File upload ≤10MB, no degradation + +## Dependencies + +- `langgraph>=0.2`, `langchain-core>=0.3`, `langchain-openai>=0.3`, `langgraph-checkpoint-postgres` (NEW — replaces sqlite) +- `gradio>=5.0`, `pdfplumber`, `openpyxl` +- `@gradio/client` npm +- Docker: new `ss-tools-agent` container, nginx proxy `/api/agent/gradio` +- Deprecated: `backend/src/api/routes/assistant/_tool_registry.py` → Tombstone + +#endregion AgentChat.Spec diff --git a/specs/033-gradio-agent-chat/tasks.md b/specs/033-gradio-agent-chat/tasks.md new file mode 100644 index 00000000..afb87a11 --- /dev/null +++ b/specs/033-gradio-agent-chat/tasks.md @@ -0,0 +1,319 @@ +#region AgentChat.Tasks [C:3] [TYPE ADR] [SEMANTICS tasks,implementation,agent-chat] +@BRIEF Implementation tasks for Gradio Agent Chat — phased by user story, dependency-ordered, with contract inlining for C3+ functions. + +## Phase 1: Setup (shared infrastructure) + +- [ ] T001 [P] Add `gradio>=5.0`, `langgraph>=0.2`, `langchain-core>=0.3`, `langchain-openai>=0.3`, `langgraph-checkpoint-postgres`, `pdfplumber` to `backend/requirements.txt` +- [ ] T002 [P] Add `@gradio/client` to `frontend/package.json` → `npm install` +- [ ] T003 [P] Create `backend/src/agent/` directory with `__init__.py` +- [ ] T004 [P] Create `backend/tests/test_agent/` directory with `__init__.py` +- [ ] T005 [P] Create `frontend/src/routes/agent/` directory +- [ ] T006 [P] Create `frontend/src/types/agent.ts` — TypeScript DTOs for conversation/message/stream metadata. No `agent-ws.ts`. +- [ ] T007 Create `docker/Dockerfile.agent` — Python 3.11-slim, installs gradio/langchain/langchain-openai/pdfplumber from requirements. No shared volume mount needed — Gradio calls FastAPI tools via HTTP. + RATIONALE Tool execution via REST (FR-004 revised) — Gradio container has no DB connection + +- [ ] T008 Update `docker/docker-compose.yml` — add `ss-tools-agent` service (port 7860 internal, NOT exposed to host). Env vars: LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, FASTAPI_URL=http://ss-tools-api:8000, SERVICE_JWT, JWT_SECRET (shared with FastAPI for stateless JWT validation). + +- [ ] T009 Update `docker/nginx.conf` — add reverse proxy location `/api/agent/gradio` → `http://ss-tools-agent:7860`. Forward `Authorization` header from browser request. + RATIONALE Browser cannot resolve Docker service names; Gradio must be proxied through nginx + +## Phase 2: Foundational (data layer — blocking for all stories) + +- [ ] T010 Implement `backend/src/models/agent.py` — SQLAlchemy models `AgentConversation` (id, user_id FK→users.id, title, is_archived, created_at, updated_at) and `AgentMessage` (id, conversation_id FK→agent_conversations.id, role, text, state, tool_calls JSON, attachments JSON, created_at). Use `uuid4_str` for primary keys. + @DATA_CONTRACT Models.Agent — see data-model.md §2 + +- [ ] T011 Implement `backend/src/schemas/agent.py` — Pydantic schemas: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `HistoryResponse`, `DeleteResponse`. Match `data-model.md` §3 exactly. + @DATA_CONTRACT Schemas.Agent + +- [ ] T012 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `ToolCall`, `AttachmentMeta`, `HistoryResponse`, `DeleteResponse`. MUST match `backend/src/schemas/agent.py` field-for-field. + @DATA_CONTRACT AgentTypes ↔ Schemas.Agent (cross-stack) + +- [ ] T013 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces for conversation/message DTOs matching `backend/src/schemas/agent.py`. `agent-ws.ts` NOT created — no custom WebSocket protocol. + +- [ ] T014 Generate Alembic migration for `agent_conversations` and `agent_messages` tables → `cd backend && source .venv/bin/activate && alembic revision --autogenerate -m "add agent conversations"` + +## Phase 3: Story 1 — Gradio Agent Engine with Streaming Chat (P1) 🎯 MVP + +### Backend + +- [ ] T015 [US1] Implement `backend/src/agent/app.py` — Gradio `gr.ChatInterface(fn=agent_handler, type="messages", multimodal=True, additional_inputs=[gr.Textbox("conversation_id"), gr.Textbox("action")], examples=[["Покажи дашборды", null, null], ["Статус системы", null, null]], concurrency_limit=10)`. Handler: `agent_handler(message, history, request: gr.Request, conversation_id=None, action=None)`. On `action="confirm"/"deny"`: resume graph from checkpoint. Per-user lock via in-memory dict keyed by user_id. + +- [ ] T016 [US1] Implement `backend/src/agent/langgraph_setup.py` — `create_react_agent(model, tools, checkpointer=PostgresSaver(os.getenv("DATABASE_URL")))`. Define dangerous tools in `DANGEROUS_TOOLS` set. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. Wrap with `RunnableWithMessageHistory` for auto history load/save (thread_id = conversation_id). + @POST Compiled StateGraph with checkpointer, interrupt_before, message history + RATIONALE langgraph provides native interrupt/resume; no custom HITL middleware needed + +- [ ] T017 [US1] Implement agent_handler yield loop in `app.py` — `async for event in graph.astream_events({"messages": [HumanMessage(content=message["text"])]}, config={"configurable": {"thread_id": conversation_id}}): yield to_chatmessage(event)`. Tool events → `ChatMessage(metadata={"type":"tool_start", "tool":..., "input":...})`. Interrupt events → `ChatMessage(metadata={"type":"confirm_required", "prompt":..., "thread_id":conversation_id})`. + @POST Each LangGraph event converted to ChatMessage with structured metadata; frontend receives typed objects + +### Frontend + +- [ ] T018 [US1] Adapt `AssistantChatPanel.svelte` — `Client.connect("/api/agent/gradio")`, `submit("/chat", message, [conversationId, null])`. Iterate `for await (event)`: `event.data` is `ChatMessage` with structured `metadata`. Render tokens, tool cards, confirmation cards based on `metadata.type`. + @UX_STATE idle, streaming, awaiting_confirmation, error, disconnected + +- [ ] T019 [US1] Implement `AgentChatModel.sendMessage()` — `client.submit("/chat", {text: message, files}, [conversationId, null])`, iterate events, append tokens. `streamingState`: idle→streaming. + +- [ ] T020 [US1] Implement `AgentChatModel.cancelGeneration()` — `submission.cancel()`. + +- [ ] T021 [US1] Implement metadata handler in `AgentChatModel._onStreamData(msg)` — switch on `msg.metadata.type`: stream_token→append, tool_start→card+spinner, tool_end→checkmark, tool_error→cross, confirm_required→confirmation card, confirm_resolved→collapse card, error→error card. + +- [ ] T022 [US1] Implement Gradio connection lifecycle — `Client.connect()`, retry on disconnect (5×5s), `connectionState` atom. + +### Verification + +- [ ] T024 [US1] Write `backend/tests/test_agent/test_agent_handler.py` — test Gradio handler: empty message returns immediately, streaming yields tokens, cancel stops generator, LLM unavailable yields error event. +- [ ] T025 [US1] Write `frontend/src/lib/models/__tests__/AgentChatModel.test.ts` — L1 model tests (no DOM render): sendMessage transitions state, cancelGeneration resets, disconnect/reconnect state machine, invalid state transitions rejected. +- [ ] T026 [US1] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v` +- [ ] T027 [US1] Verify: `cd frontend && npm run test -- --reporter=verbose` +- [ ] T028 [US1] Verify UX against `ux_reference.md`: streaming tokens appear in real time, Stop button halts generation, connection dot green/red/yellow. + +--- + +## Phase 4: Story 2 — Autonomous Tool Selection via Agent (P1) 🎯 MVP + +### Backend + +- [ ] T026 [US2] Implement `backend/src/agent/tools.py` — native LangChain `@tool`-декорированные функции. Каждый `@tool`: Pydantic `BaseModel` для args_schema, docstring для description, внутри → HTTP-вызов к FastAPI REST с service JWT. Пример: `@tool async def search_dashboards(query: str, environment: str = None) → str`. Список тулов: все существующие @assistant_tool операции. + @DATA_CONTRACT Each @tool: Pydantic args → FastAPI REST call → JSON string result + RATIONALE Native @tool replaces dual @assistant_tool + StructuredTool wrapping + +- [ ] T027 [US2] Implement `backend/src/agent/middleware.py` — `LoggingMiddleware` (логирует tool-call события в audit trail), `ConfirmationRiskMiddleware` (определяет interrupt_on список из конфига TOOL_RISK_LEVELS). + +- [ ] T028 [US2] Register tools in `langgraph_setup.py` — `DANGEROUS_TOOLS = {"deploy_dashboard", "execute_migration", "commit_changes"}`. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. All tools from `tools.py` registered as graph nodes. + @POST Agent with tools, interrupt, logging, checkpoints, history — all LangChain-native + +- [ ] T029 [US2] Implement interrupt handler in `app.py` — when graph pauses at `interrupt_before` node: yield `ChatMessage(metadata={"type":"confirm_required", ...})`. On second submit with `additional_inputs[1]="confirm"/"deny"`: invoke graph with `Command(resume={"action": action}, config={"thread_id": conversation_id})`. Graph resumes from checkpoint. + +- [ ] T031 [US2] Implement confirmation rendering in `AssistantChatPanel.svelte` — on `metadata.type="confirm_required"`: warning card + Confirm/Deny. Confirm → `submit("/chat", {text:"confirm"}, [conversationId, "confirm"])`. Deny → `submit("/chat", {text:"deny"}, [conversationId, "deny"])`. On `metadata.type="confirm_resolved"`: collapse card. + +### Verification + +- [ ] T034 [US2] Write `backend/tests/test_agent/test_langchain_tools.py` — test: REST-calling tools wrap correctly, agent selects tool, agent handles tool failure gracefully. +- [ ] T035 [US2] Write `backend/tests/test_agent/test_confirmations.py` — test: confirmation created, confirmed, denied, expired flows. +- [ ] T036 [US2] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "tool or confirm"` +- [ ] T037 [US2] Verify UX: confirmation card appears for dangerous ops, confirm via second `submit()` with `__resume__`, deny yields `⏹️`. + +--- + +## Phase 5: Story 3 — Tool-Call Visibility in Chat (P2) + +### Frontend + +- [ ] T039 [US3] Implement `frontend/src/lib/components/assistant/ToolCallCard.svelte` — inline card within assistant message. Props: `{tool, input, output?, error?, status}`. Visual: tool name in mono font, spinner (executing)/checkmark (done)/cross (error). Expandable to show input params and output result. + @UX_STATE executing (spinner), completed (checkmark, expandable result), failed (cross, error detail) + @RELATION DEPENDS_ON -> $lib/ui/Icon (existing) + @RELATION DEPENDS_ON -> $lib/ui/Button variant="ghost" (existing, for expand/collapse) + Design tokens: card=`bg-surface-muted border border-border rounded-lg`, tool name=`text-text-muted text-xs font-mono`, spinner=`animate-spin text-primary`, checkmark=`text-success`, cross=`text-destructive` + +- [ ] T040 [US3] Integrate ToolCallCard into `AssistantChatPanel.svelte` — render ToolCallCard when `metadata.type="tool_start"/"tool_end"/"tool_error"`. On tool_start: card+spinner. On tool_end: checkmark+result. On tool_error: cross+error. + +- [ ] T041 [US3] Implement metadata dispatch in `AgentChatModel._onStreamData(msg)` — handle `tool_start` (push card), `tool_end` (update status), `tool_error` (update status). Update `activeToolCalls[]`. + +### Verification + +- [ ] T042 [US3] Write `frontend/src/lib/components/assistant/__tests__/ToolCallCard.test.ts` — test: renders tool name, shows spinner in executing state, shows checkmark on completed, shows cross on error, expand/collapse toggles detail visibility. +- [ ] T043 [US3] Verify: `cd frontend && npm run test -- --reporter=verbose` +- [ ] T044 [US3] Verify UX: multi-tool query → each tool card appears inline, can be expanded, shows real-time status. + +--- + +## Phase 6: Story 4 — Multi-Turn Conversation Context (P2) + +### Backend + +- [ ] T045 [US4] Verify `RunnableWithMessageHistory` auto-loads context from PostgreSQL per `thread_id=conversation_id`. No manual `history` injection needed. + +### Frontend + +- [ ] T047 [US4] Implement `AgentChatModel.createConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — clear messages, reset streamingState→idle, set currentConversationId=null. Existing conversation is NOT lost (persisted in DB). + @ACTION createConversation(): starts clean conversation + @POST messages=[], streamingState="idle", partialText="" + @TEST_EDGE: create_during_streaming→cancel_first_then_create + +### Verification + +- [ ] T048 [US4] Write integration test: send message 1 referencing entity, send message 2 with pronoun → agent resolves pronoun from context. +- [ ] T049 [US4] Write test: conversation exceeds 4000 tokens → older messages summarized, recent detail preserved. +- [ ] T050 [US4] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "memory or context"` +- [ ] T051 [US4] Verify UX: follow-up question resolves prior context, new conversation starts clean. + +--- + +## Phase 7: Story 5 — Conversation Persistence and Management (P3) + +### Backend + +- [ ] T052 [US5] Implement `backend/src/api/routes/agent_conversations.py` — FastAPI router: `POST /api/assistant/conversations` (create), `GET /api/assistant/conversations` (paginated list + search), `GET /api/assistant/history` (paginated messages), `POST /api/assistant/conversations/{id}/messages` (append batch after stream_end), `DELETE /api/assistant/conversations/{id}` (soft-delete: is_archived=true). +- [ ] T053 [US5] Implement `POST /api/assistant/conversations`: + @PRE User authenticated, title from first message (truncated 60 chars) + @POST ConversationItem(id, title, created_at) — HTTP 201 + @TEST_EDGE: unauthenticated→401 + @PRE User authenticated (via existing FastAPI auth dependency) + @POST ConversationListResponse(items[], has_next, active_total, archived_total) + @DATA_CONTRACT Input: (page, page_size, include_archived?, search?) → Output: ConversationListResponse + @TEST_EDGE: empty_result→items=[], totals=0, no_error — filter_invalid→ignored +- [ ] T054 [US5] Implement `AgentChat.Api.GetHistory` in `backend/src/api/routes/agent_conversations.py`: + @PRE conversation_id valid + @POST HistoryResponse(items[], has_next, conversation_id) + @TEST_EDGE: invalid_conversation_id→404 — empty_conversation→items=[], no_error +- [ ] T055 [US5] Implement `AgentChat.Api.DeleteConversation` in `backend/src/api/routes/agent_conversations.py`: + @PRE conversation_id valid, user owns conversation + @POST DeleteResponse(deleted=true), conversation is_archived=true + @SIDE_EFFECT Sets is_archived=true, cascades to messages (soft delete) + @TEST_EDGE: already_deleted→404 — not_owner→403 + +- [ ] T056 [US5] Register agent_conversations router in `backend/src/app.py` — prefix `/api/assistant` (shares namespace with existing assistant routes for backward compat FR-020). + +### Frontend + +- [ ] T057 [US5] Implement `AgentChatModel.loadConversations()` and `loadHistory()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — call existing `getAssistantConversations()` and `getAssistantHistory()` from `frontend/src/lib/api/assistant.ts`. Infinite scroll for both: append on next page, reset on new filter. + @ACTION loadConversations(reset?): fetches from REST, manages pagination + @POST conversations array updated, conversationsHasNext flag set + @ACTION loadHistory(conversationId?): fetches messages from REST + @POST messages array populated, historyHasNext flag set + @TEST_EDGE: api_error→error state with retry, empty_response→empty state + +- [ ] T058 [US5] Implement `AgentChatModel.deleteConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — optimistic removal from conversations array, DELETE via REST, rollback on failure with toast. + @ACTION deleteConversation(id): optimistic delete + @POST removed from list, toast on success/error + @TEST_EDGE: api_failure→rollback_reinsert, active_conversation_deleted→switch_to_next + +- [ ] T059 [US5] Implement `frontend/src/lib/components/assistant/ConversationList.svelte` — sidebar list (240px) with: `` for search (debounced 300ms), grouped by date on client (`$derived` from `conversations`), infinite scroll (IntersectionObserver), delete button (with `confirm()` before calling model.deleteConversation), active state highlight (`bg-surface-muted`). Skeleton on load. + @UX_STATE loading (skeleton), loaded (grouped list), empty ("Нет диалогов"), error (retry) + @RELATION DEPENDS_ON -> $lib/ui/Input (existing), $lib/ui/Button variant="ghost" (existing), $lib/ui/Icon (existing) + @RELATION BINDS_TO -> AgentChat.Model + Design tokens: sidebar=`bg-surface-card border-r border-border`, skeleton=`animate-pulse bg-surface-muted` + +- [ ] T060 [US5] Integrate ConversationList into `AssistantChatPanel.svelte` (drawer header as dropdown) and into `/agent` page (left sidebar). + +### Verification + +- [ ] T061 [US5] Write `backend/tests/test_agent/test_conversation_api.py` — test: list returns paginated, search filters by title, history returns messages, delete archives conversation, 404 on invalid id, 403 on other user's conversation. +- [ ] T062 [US5] Write `frontend/src/lib/components/assistant/__tests__/ConversationList.test.ts` — test: renders conversations, groups by date, search filters, delete shows confirm and removes, error shows retry. +- [ ] T063 [US5] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_conversation_api.py -v` +- [ ] T064 [US5] Verify: `cd frontend && npm run test -- --reporter=verbose` + +--- + +## Phase 8: Story 6 — File Upload and Document Analysis (P2) + +### Backend + +- [ ] T065 [US6] Implement `backend/src/agent/document_parser.py` — `parse_pdf(file_path)` using pdfplumber (primary, extracts text + tables), PyPDF2 (fallback for encrypted). `parse_xlsx(file_path)` using openpyxl (sheet names → 2D cell arrays). + @PRE File exists on disk, valid extension + @POST Returns extracted text (PDF) or structured dict (XLSX) + @TEST_EDGE: encrypted_pdf→ParseError with message, password_xlsx→ParseError, empty_file→empty_string (no crash), unsupported_extension→ParseError + +- [ ] T066 [US6] Integrate document parser into agent_handler in `backend/src/agent/app.py` — before running agent, detect file attachments (Gradio `gr.File` component), parse content via T065, inject as system message: `"Вот содержимое файла {name}:\n{parsed_text}"`. + @PRE File uploaded via Gradio multimodal input + @POST Parsed content injected as system message in conversation + +- [ ] T067 [US6] Add file size validation (10MB limit) in agent_handler — reject oversized files before parsing, return error message to user. + @PRE File attached to message + @POST File accepted if ≤10MB, rejected with error message if exceeded + +### Frontend + +- [ ] T068 [US6] Add file upload UI to `AssistantChatPanel.svelte` — paperclip icon button (using `$lib/ui/Icon name="paperclip"`), hidden ``, preview chip below input showing filename + size. Validate format and size before upload (10MB client-side check). + @UX_FEEDBACK file chip appears below input, invalid format→toast error, oversized→toast error + @RELATION DEPENDS_ON -> $lib/ui/Icon (existing) + +- [ ] T069 [US6] Pass file attachments through `AgentChatModel.sendMessage()` — append file data to WebSocket message payload, show parsing indicator on file chip while agent processes. + @TEST_EDGE: upload_during_streaming→rejected, upload_empty_file→validation_error + +### Verification + +- [ ] T070 [US6] Write `backend/tests/test_agent/test_document_parser.py` — test: PDF extracts text, PDF with tables preserves structure, XLSX parses all sheets, encrypted/empty/invalid files handled gracefully. +- [ ] T071 [US6] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_document_parser.py -v` +- [ ] T072 [US6] Verify UX: upload PDF → agent analyzes content, upload XLSX → agent reads tabular data, oversized file → rejected. + +--- + +## Phase 9: Polish & Cross-Cutting Verification + +### Frontend + +- [ ] T073 Implement `frontend/src/routes/agent/+page.svelte` — full-page agent chat at `/agent` route. Two-column layout: `` (240px left sidebar) + `` (right area). `` title="Агент-чат". Protected route (auth check in +layout.svelte). + @RELATION BINDS_TO -> AgentChat.Model (same instance) + @RELATION DEPENDS_ON -> $lib/ui/PageHeader (existing) + @RELATION DEPENDS_ON -> AgentChat.ConversationList + @RELATION DEPENDS_ON -> AgentChat.Panel + Design tokens: page=`bg-surface-page max-w-7xl mx-auto`, two-column=`flex`, sidebar=`w-60 shrink-0 bg-surface-card border-r border-border`, chat=`flex-1` + +- [ ] T074 Implement `frontend/src/lib/components/assistant/ConnectionIndicator.svelte` — green/red dot binding to `model.connectionDotColor` `$derived`. Tooltip: "Подключено"/"Недоступно". + @RELATION BINDS_TO -> AgentChat.Model + Design tokens: green=`bg-success w-2 h-2 rounded-full`, red=`bg-destructive w-2 h-2 rounded-full` + +- [ ] T075 Implement multi-tab gate in `AgentChatModel.sendMessage()` — before sending, call `POST /api/agent/conversations/{id}/active` to check for existing session. If active → reject with toast "Диалог активен в другой вкладке". + @TEST_EDGE: second_tab_send→rejected_with_toast, first_tab_send→proceeds + +- [ ] T076 Add "Expand to /agent" button in drawer header — navigates to `/agent` route preserving current conversation context via store. + @UX_FEEDBACK smooth navigation, conversation state preserved + +- [ ] T077 Implement conversation auto-title — on first agent response, set `AgentConversation.title` from truncated first user message (60 chars) during conversation creation in T052. No separate PATCH endpoint needed — title is set at insert time. + @POST Conversation.title populated at creation + +### Backend + +- [ ] T078 Add audit logging to agent_handler — every user message and agent response writes to existing `assistant_audit` table: `{user_id, conversation_id, decision: "message_sent"/"tool_called"/"confirmed"/"denied", message, payload}`. + @SIDE_EFFECT Writes to assistant_audit table + RATIONALE FR-010: all agent interactions logged for auditability + +- [ ] T079 Implement Gradio healthcheck endpoint — `GET /health` returns `{"status":"ok","uptime":...}` for Docker healthcheck. + +- [ ] T080 [P] Implement `backend/src/api/routes/auth.py` — internal endpoint `POST /api/auth/service-token` that accepts a static service secret (env `SERVICE_TOKEN_SECRET`) and returns a long-lived JWT with role `agent`. Used by Gradio container at startup for Gradio→FastAPI HTTP calls. + @DATA_CONTRACT Input: {service_secret} → Output: {access_token, expires_in} + @TEST_EDGE: invalid_secret→401, expired_token→401_on_next_call + RATIONALE FR-023: Gradio→FastAPI calls authenticated via JWT, not separate API key mechanism + +- [ ] T081 [P] Add i18n keys to `frontend/src/lib/i18n/locales/ru/assistant.json` and `frontend/src/lib/i18n/locales/en/assistant.json` — new keys: `stop`, `tool_executing`, `tool_completed`, `tool_failed`, `confirm_required`, `confirm_expired`, `confirm_retry`, `file_upload`, `file_parse_error`, `file_unsupported`, `file_too_large`, `connection_lost`, `reconnecting`, `manual_reconnect`, `archive_dialog`. + RATIONALE FR-013: Russian + English i18n for all new UX strings (8+ in UX reference §3-4) + +- [ ] T082 Implement LLM malformed JSON retry in `backend/src/agent/app.py` agent_handler — catch `OutputParserException` from LangChain, retry once with `"Respond with valid JSON only. Previous response was malformed."` appended to prompt. If still malformed, yield `stream_error` with clarification request. + @TEST_EDGE: first_retry_succeeds→normal_flow, second_failure→stream_error_with_clarification + RATIONALE Spec Edge Cases: "LLM returns malformed tool-call JSON → retry once with stricter prompt" + +- [ ] T083 Implement rapid-fire message queue in `AgentChatModel.sendMessage()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — if `streamingState !== "idle"`, enqueue message; process queue sequentially when state returns to idle. Show queue position badge if >1 pending. + @TEST_EDGE: send_during_streaming→queued, send_during_sending→queued, queue_drains_on_idle + RATIONALE Spec Edge Cases: "Simultaneous rapid-fire messages → queue and process sequentially" + +### Verification (cross-cutting) + +- [ ] T084 [P] Backend full test suite: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/ -v` +- [ ] T085 [P] Frontend full test suite: `cd frontend && npm run test -- --reporter=verbose` +- [ ] T086 [P] Backend lint: `cd backend && python -m ruff check src/agent/ src/api/routes/agent_conversations.py src/models/agent.py src/schemas/agent.py` +- [ ] T087 [P] Frontend lint: `cd frontend && npm run lint` +- [ ] T088 [P] Frontend build: `cd frontend && npm run build` +- [ ] T089 Semantic audit — agent contracts: `axiom_semantic_validation audit_contracts file_path="backend/src/agent/"` +- [ ] T090 UX reference validation — verify all states from `contracts/ux/agent-chat-ux.md` (15 FSM states) are reachable and rendered correctly via browser or vitest @UX_TEST scenarios. +- [ ] T091 Rejected-path regression test — verify that `/api/assistant` REST endpoints still function (FR-020 backward compat). Existing `backend/tests/test_assistant_api.py` passes unmodified. +- [ ] T092 Verify ADR guardrails: `@REJECTED full Svelte replacement` → SvelteKit routes intact; `@REJECTED custom SSE/WebSocket` → @gradio/client used exclusively; `@REJECTED separate auth mechanism` → JWT reused. + +--- + +## Task Summary + +| Phase | Stories | Tasks | Parallel | +|-------|---------|:-----:|:--------:| +| P1 — Setup | — | T001-T009 | 6 | +| P2 — Foundational | — | T010-T014 | 2 | +| P3 — US1 (Streaming) | P1 🎯 | T015-T028 | — | +| P4 — US2 (Tools) | P1 🎯 | T029-T038 | — | +| P5 — US3 (Visibility) | P2 | T039-T044 | — | +| P6 — US4 (Context) | P2 | T045-T051 | — | +| P7 — US5 (Persistence) | P3 | T052-T064 | — | +| P8 — US6 (Files) | P2 | T065-T072 | — | +| P9 — Polish + Gaps | — | T073-T092 | 8 | +| **Total** | | **92** | **16** | + +## Story Independence + +| Story | Independent Test | +|-------|------------------| +| **US1** | Launch Gradio, connect from Svelte, send message → streaming tokens appear | +| **US2** | Send multi-tool query → agent selects correct tools autonomously | +| **US3** | Multi-tool query → tool-call cards visible inline, expandable | +| **US4** | Follow-up with pronoun → agent resolves from prior context | +| **US5** | 3 conversations, restart app → all 3 appear, resume works | +| **US6** | Upload PDF → agent extracts and analyzes content | + +#endregion AgentChat.Tasks diff --git a/specs/033-gradio-agent-chat/ux_reference.md b/specs/033-gradio-agent-chat/ux_reference.md new file mode 100644 index 00000000..367d11af --- /dev/null +++ b/specs/033-gradio-agent-chat/ux_reference.md @@ -0,0 +1,33 @@ +#region AgentChat.UxReference [C:3] [TYPE ADR] [SEMANTICS ux,reference,agent-chat,final] +@BRIEF Final UX reference — Gradio `submit()`, LangChain v1 HITL, structured metadata, Archive UX. + +## 1. Persona + +Platform operator in ss-tools Svelte interface. Opens chat (drawer or `/agent` page). Streams responses, confirms dangerous ops via inline card, uploads files for analysis. + +## 2. Happy Path + +`submit("/chat", {message}, conversation_id)`. Tokens stream in real time. Tool cards appear from structured metadata. Confirmation card for dangerous ops — Confirm via second `submit()`. Archive conversations via delete (soft-delete). + +## 3. Key Elements + +- **Input**: Textarea + paperclip (PDF/XLSX/JSON/CSV/PNG/JPEG) +- **Streaming**: Text character-by-character. Tool cards from `metadata.type`. +- **Stop**: `submission.cancel()` — native. +- **Confirmation**: Warning card from `metadata.type="confirm_required"`. Confirm → second `submit()`. +- **Conversation Switcher**: Search, date-grouped, infinite scroll. +- **Connection**: Green/red dot. + +## 4. Error Scenarios + +- **Gradio down**: Dot red, retry 5×5s. +- **LLM error**: Stream `metadata.type="error"` → card + retry. +- **Tool failure**: `metadata.type="tool_error"` → cross + detail. +- **File parse**: Error chip: "Не удалось прочитать". +- **Multi-tab**: REST gate rejects second tab. + +## 5. Gradual Transition + +Phase 1: Gradio + Svelte submit(). Phase 2: Old registry → @DEPRECATED. Phase 3: REST preserved (FR-022). + +#endregion AgentChat.UxReference