tasks ready 1
This commit is contained in:
@@ -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)
|
- 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)
|
- 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)
|
- 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)
|
- 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
|
Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5): Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## 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)
|
- 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)
|
- 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)
|
|
||||||
|
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- `.opencode/skills/semantics-svelte/SKILL.md`
|
- `.opencode/skills/semantics-svelte/SKILL.md`
|
||||||
- `.opencode/skills/semantics-testing/SKILL.md`
|
- `.opencode/skills/semantics-testing/SKILL.md`
|
||||||
- `.specify/templates/plan-template.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`
|
- relevant `docs/adr/*.md`
|
||||||
|
|
||||||
3. **Execute the planning workflow** using the template structure:
|
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.
|
- 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 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: 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.
|
- 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.
|
- 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
|
- belief runtime instrumentation for C4/C5 flows
|
||||||
- semantic validation boundaries and static verification workflow
|
- 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
|
||||||
|
- `<screen>-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:
|
Write `research.md` with concise sections:
|
||||||
- Decision
|
- Decision
|
||||||
- Rationale
|
- 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.
|
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/<feature>/fixtures/
|
||||||
|
├── manifest.md # Fixture index with GRACE contracts
|
||||||
|
├── api/
|
||||||
|
│ ├── <contract>_valid.json
|
||||||
|
│ ├── <contract>_missing_field.json
|
||||||
|
│ ├── <contract>_invalid_type.json
|
||||||
|
│ ├── <contract>_external_fail.json
|
||||||
|
│ └── <contract>_rejected_path.json
|
||||||
|
└── model/
|
||||||
|
├── <model>_valid.json
|
||||||
|
├── <model>_edge_case.json
|
||||||
|
└── <model>_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 `<contract_snake>_<edge>.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_<Domain>.<Name>` — 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
|
### Quickstart Output
|
||||||
|
|
||||||
Generate `quickstart.md` using real repository verification paths:
|
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`
|
- Frontend lint: `cd frontend && npm run lint`
|
||||||
- Docker: `docker compose up --build`
|
- 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.<Domain>.<Name>`
|
||||||
|
- 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 "<contract>" traceability.md` replaces `impact_analysis`
|
||||||
|
|
||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
- Use absolute paths in workflow execution.
|
- Use absolute paths in workflow execution.
|
||||||
|
|||||||
@@ -173,3 +173,29 @@ Before finalizing `tasks.md`, verify that:
|
|||||||
- no task text schedules a rejected path
|
- no task text schedules a rejected path
|
||||||
- story tasks remain executable within the actual Python/Svelte project structure
|
- story tasks remain executable within the actual Python/Svelte project structure
|
||||||
- at least one explicit verification task protects against rejected-path regression
|
- at least one explicit verification task protects against rejected-path regression
|
||||||
|
|
||||||
|
### Fixture Materialization Tasks
|
||||||
|
|
||||||
|
If `/speckit.plan` generated canonical fixtures in `specs/<feature>/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/<feature>/fixtures/api/ into backend/tests/fixtures/<domain>/
|
||||||
|
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/<feature>/fixtures/model/ into frontend/src/lib/models/__fixtures__/<model>/
|
||||||
|
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
|
||||||
|
|||||||
366
.opencode/command/speckit.ux.md
Normal file
366
.opencode/command/speckit.ux.md
Normal file
@@ -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/<screen>-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/<Domain>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
|
||||||
|
|
||||||
|
### `<screen>-ux.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#region <Screen>Ux [C:3] [TYPE ADR] [SEMANTICS ux,<domain>,<screen>]
|
||||||
|
@defgroup Ux UX contract for <Screen>.
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### `<Domain>Model.svelte.ts` — generated code
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/src/lib/models/<Domain>Model.svelte.ts
|
||||||
|
// #region <Domain>.Model [C:4] [TYPE Model] [SEMANTICS <domain>,<feature>,screen-model]
|
||||||
|
// @defgroup <Domain> <One-line from decisions>.
|
||||||
|
// @INVARIANT <from Phase 2-3 decisions>
|
||||||
|
// @STATE <FSM states from Phase 2>
|
||||||
|
// @ACTION <from Phase 3 interaction decisions>
|
||||||
|
// @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 <Domain>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<void> {
|
||||||
|
this.screenState = "loading";
|
||||||
|
this.error = null;
|
||||||
|
log("<Domain>.Model", "REASON", "Loading items");
|
||||||
|
try {
|
||||||
|
const res: ListResponse = await requestApi("/api/...");
|
||||||
|
this.items = res.data;
|
||||||
|
this.screenState = this.items.length === 0 ? "empty" : "loaded";
|
||||||
|
log("<Domain>.Model", "REFLECT", "Items loaded", { count: this.items.length });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.error = e instanceof Error ? e.message : "Load failed";
|
||||||
|
this.screenState = "error";
|
||||||
|
log("<Domain>.Model", "EXPLORE", "Load failed", {}, this.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retry(): Promise<void> { await this.load(); }
|
||||||
|
|
||||||
|
// TODO: implement remaining actions from Phase 3 decisions
|
||||||
|
// Each action throws until implemented — L1-testable immediately
|
||||||
|
}
|
||||||
|
// #endregion <Domain>.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`
|
||||||
@@ -67,11 +67,12 @@ All generated contracts (specs, code, tests) MUST be optimized for the attention
|
|||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.implement
|
/speckit.specify → /speckit.clarify → /speckit.ux → /speckit.plan → /speckit.tasks → /speckit.implement
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rules:**
|
**Rules:**
|
||||||
- No phase is skipped when `[NEEDS CLARIFICATION]` markers remain
|
- 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
|
- Contract mutation: preview → apply, never immediate apply
|
||||||
- All feature artifacts in `specs/<feature>/`, never in `.kilo/` or `.ai/`
|
- All feature artifacts in `specs/<feature>/`, never in `.kilo/` or `.ai/`
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,17 @@ specs/[###-feature]/
|
|||||||
├── research.md # Phase 0 output (/speckit.plan command)
|
├── research.md # Phase 0 output (/speckit.plan command)
|
||||||
├── data-model.md # Phase 1 output (/speckit.plan command)
|
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||||
├── quickstart.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)
|
├── 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
|
||||||
|
│ ├── <screen>-ux.md # Per-screen UX contracts
|
||||||
|
│ └── design-tokens.md # Applied tokens & reuse
|
||||||
|
└── tasks.md # Phase 2 output (/speckit.tasks command)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Source Code (repository root)
|
### Source Code (repository root)
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ Examples of foundational tasks (adjust based on your project):
|
|||||||
- [ ] T007 Create base models/entities that all stories depend on
|
- [ ] T007 Create base models/entities that all stories depend on
|
||||||
- [ ] T008 Configure error handling and logging infrastructure
|
- [ ] T008 Configure error handling and logging infrastructure
|
||||||
- [ ] T009 Setup environment configuration management
|
- [ ] T009 Setup environment configuration management
|
||||||
|
- [ ] T00A [P] Materialize canonical fixtures from specs/<feature>/fixtures/api/ into backend/tests/fixtures/
|
||||||
|
- [ ] T00B [P] Materialize canonical fixtures from specs/<feature>/fixtures/model/ into frontend/src/lib/models/__fixtures__/
|
||||||
|
|
||||||
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
**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/`
|
- **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)**
|
- **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
|
- **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/<feature>/fixtures/`** — materialize to `tests/` via tasks, never edit canonical source in test directories
|
||||||
|
|||||||
325
frontend/src/lib/models/AgentChatModel.svelte.ts
Normal file
325
frontend/src/lib/models/AgentChatModel.svelte.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
output?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
prompt?: string;
|
||||||
|
thread_id?: string;
|
||||||
|
result?: "confirmed" | "denied";
|
||||||
|
code?: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCall {
|
||||||
|
tool: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
output?: Record<string, unknown>;
|
||||||
|
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<GradioClient["submit"]> | 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<typeof setInterval> | 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
154
merge_spec.py
154
merge_spec.py
@@ -9,22 +9,48 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REVIEW_PROMPT = (
|
REVIEW_PROMPT = (
|
||||||
"Другая LLM создала этот feature-пакет. Твоя задача - провести независимое "
|
"Another LLM created this feature package. Your task: conduct an independent "
|
||||||
"ортогональное spec review, оценить готовность спецификации, найти противоречия, "
|
"orthogonal spec review. Evaluate readiness, find contradictions, gaps, "
|
||||||
"пробелы, риски реализации и подготовить структурированный отчет с корректировками. "
|
"implementation risks, and prepare a structured report with corrections. "
|
||||||
"Сфокусируйся именно на review пакета спецификации, а не на переписывании реализации."
|
"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 = (
|
CANONICAL_MD_STAGES = (
|
||||||
("exact", "spec.md"),
|
# Layer 1: Requirements & UX narrative
|
||||||
("exact", "ux_reference.md"),
|
("exact", "spec.md", "SPEC — Feature Specification"),
|
||||||
("prefix", "checklists/"),
|
("exact", "ux_reference.md", "UX REFERENCE — Interaction Narrative"),
|
||||||
("exact", "plan.md"),
|
("prefix", "checklists/", "CHECKLISTS — Requirements Quality"),
|
||||||
("exact", "research.md"),
|
|
||||||
("exact", "data-model.md"),
|
# Layer 2: UX Design (from /speckit.ux)
|
||||||
("prefix", "contracts/"),
|
("exact", "contracts/ux/alternatives.md", "UX ALTERNATIVES — Design Space Explored"),
|
||||||
("exact", "quickstart.md"),
|
("exact", "contracts/ux/decisions.md", "UX DECISIONS — Final Choices"),
|
||||||
("exact", "tasks.md"),
|
("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]:
|
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()]
|
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}
|
remaining = {relative_key(path, target_dir): path for path in markdown_files}
|
||||||
|
|
||||||
ordered: list[Path] = []
|
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":
|
if stage_type == "exact":
|
||||||
path = remaining.pop(stage_value, None)
|
# Exact: only match if the file exists and hasn't been collected yet
|
||||||
if path is not None:
|
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)
|
ordered.append(path)
|
||||||
|
seen.add(path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Prefix: match all files whose relative path starts with stage_value
|
||||||
stage_matches = sorted(
|
stage_matches = sorted(
|
||||||
[
|
[
|
||||||
path
|
path
|
||||||
for relative_path, path in remaining.items()
|
for rel_path, path in remaining.items()
|
||||||
if relative_path.startswith(stage_value)
|
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:
|
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(
|
# Remaining files — append at end
|
||||||
sorted(remaining.values(), key=lambda path: relative_key(path, target_dir))
|
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
|
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")
|
specs_dir = Path("specs")
|
||||||
if not specs_dir.exists():
|
if not specs_dir.exists():
|
||||||
print("Error: 'specs' directory not found.")
|
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
|
target_dir = None
|
||||||
for item in specs_dir.iterdir():
|
for item in specs_dir.iterdir():
|
||||||
if item.is_dir() and item.name.startswith(f"{feature_number}-"):
|
if item.is_dir() and item.name.startswith(f"{feature_number}-"):
|
||||||
@@ -78,10 +120,8 @@ def merge_specs(feature_number):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not target_dir:
|
if not target_dir:
|
||||||
print(
|
print(f"Error: No directory found for feature number '{feature_number}' in 'specs/'.")
|
||||||
f"Error: No directory found for feature number '{feature_number}' in 'specs/'."
|
return None
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
feature_name = target_dir.name
|
feature_name = target_dir.name
|
||||||
now = datetime.now().strftime("%Y%m%d-%H%M%S")
|
now = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
@@ -90,7 +130,11 @@ def merge_specs(feature_number):
|
|||||||
content_blocks = [
|
content_blocks = [
|
||||||
REVIEW_PROMPT,
|
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:
|
for file_path in files_to_merge:
|
||||||
relative_path = file_path.relative_to(target_dir)
|
relative_path = file_path.relative_to(target_dir)
|
||||||
try:
|
try:
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
content = file_path.read_text(encoding="utf-8")
|
||||||
file_content = f.read()
|
|
||||||
|
|
||||||
content_blocks.append(f"--- FILE: {relative_path} ---\n")
|
|
||||||
content_blocks.append(file_content)
|
|
||||||
content_blocks.append("\n")
|
|
||||||
except Exception as e:
|
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:
|
# Determine section label
|
||||||
f.write("\n".join(content_blocks))
|
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__":
|
if __name__ == "__main__":
|
||||||
@@ -120,6 +189,7 @@ if __name__ == "__main__":
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
merge_specs(sys.argv[1])
|
merge_specs(sys.argv[1])
|
||||||
|
|
||||||
# #endregion merge_spec
|
# #endregion merge_spec
|
||||||
|
|
||||||
# #endregion MergeSpec
|
# #endregion MergeSpec
|
||||||
|
|||||||
85
specs/033-gradio-agent-chat/checklists/requirements.md
Normal file
85
specs/033-gradio-agent-chat/checklists/requirements.md
Normal file
@@ -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`.
|
||||||
30
specs/033-gradio-agent-chat/contracts/modules.md
Normal file
30
specs/033-gradio-agent-chat/contracts/modules.md
Normal file
@@ -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
|
||||||
88
specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md
Normal file
88
specs/033-gradio-agent-chat/contracts/ux/agent-chat-ux.md
Normal file
@@ -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
|
||||||
90
specs/033-gradio-agent-chat/contracts/ux/api-ux.md
Normal file
90
specs/033-gradio-agent-chat/contracts/ux/api-ux.md
Normal file
@@ -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
|
||||||
48
specs/033-gradio-agent-chat/contracts/ux/decisions.md
Normal file
48
specs/033-gradio-agent-chat/contracts/ux/decisions.md
Normal file
@@ -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
|
||||||
113
specs/033-gradio-agent-chat/contracts/ux/design-tokens.md
Normal file
113
specs/033-gradio-agent-chat/contracts/ux/design-tokens.md
Normal file
@@ -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
|
||||||
29
specs/033-gradio-agent-chat/contracts/ux/screen-models.md
Normal file
29
specs/033-gradio-agent-chat/contracts/ux/screen-models.md
Normal file
@@ -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
|
||||||
52
specs/033-gradio-agent-chat/data-model.md
Normal file
52
specs/033-gradio-agent-chat/data-model.md
Normal file
@@ -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<string, unknown>;
|
||||||
|
output?: Record<string, unknown>;
|
||||||
|
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
|
||||||
71
specs/033-gradio-agent-chat/plan.md
Normal file
71
specs/033-gradio-agent-chat/plan.md
Normal file
@@ -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)
|
||||||
57
specs/033-gradio-agent-chat/quickstart.md
Normal file
57
specs/033-gradio-agent-chat/quickstart.md
Normal file
@@ -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=<shared with FastAPI>
|
||||||
|
FASTAPI_URL=http://ss-tools-api:8000
|
||||||
|
SERVICE_TOKEN_SECRET=<for POST /api/auth/service-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#endregion AgentChat.Quickstart
|
||||||
52
specs/033-gradio-agent-chat/research.md
Normal file
52
specs/033-gradio-agent-chat/research.md
Normal file
@@ -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
|
||||||
133
specs/033-gradio-agent-chat/spec.md
Normal file
133
specs/033-gradio-agent-chat/spec.md
Normal file
@@ -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
|
||||||
319
specs/033-gradio-agent-chat/tasks.md
Normal file
319
specs/033-gradio-agent-chat/tasks.md
Normal file
@@ -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: `<Input>` 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 `<input type="file" accept=".pdf,.xlsx,.json,.csv,.txt,.png,.jpeg">`, 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: `<ConversationList>` (240px left sidebar) + `<AssistantChatPanel variant="embedded">` (right area). `<PageHeader>` 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
|
||||||
33
specs/033-gradio-agent-chat/ux_reference.md
Normal file
33
specs/033-gradio-agent-chat/ux_reference.md
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user