106 lines
4.8 KiB
Markdown
106 lines
4.8 KiB
Markdown
#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("${window.location.origin}/api/agent/gradio")` → Vite/nginx same-origin proxy → Gradio. JWT forwarded.
|
||
|
||
**Primary submit**: `client.submit("/chat", {text, files}, [conversation_id, null])` — three-arg API. `additional_inputs=[conversation_id, action]` maps to Gradio textboxes. Handler: `handler(message, history, request, conversation_id, action)`.
|
||
|
||
**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, null])
|
||
→ handler: graph.astream_events(config={"thread_id": conversation_id})
|
||
→ interrupt_before fires → checkpoint saved (PostgreSQL)
|
||
→ handler: yield {metadata: {type: "confirm_required", thread_id: conversation_id}}
|
||
→ Svelte: renders confirmation card (primary stream ends)
|
||
|
||
Confirm: submit("/chat", {message: "confirm", action: "confirm", conversation_id}, [conversation_id, "confirm"])
|
||
→ handler: detects action="confirm" → loads checkpoint by thread_id
|
||
→ recreates graph with interrupt_before=[]
|
||
→ continues checkpoint stream with config={"thread_id": conversation_id}
|
||
→ graph resumes from checkpoint → new stream begins
|
||
|
||
Deny: submit("/chat", {message: "deny", action: "deny", conversation_id}, [conversation_id, "deny"])
|
||
→ handler: uses checkpoint thread_id and interrupt_before=[]
|
||
→ yields confirm_resolved(result="denied") → agent responds
|
||
```
|
||
|
||
---
|
||
|
||
## 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, ...}
|
||
```
|
||
|
||
## Tool Schema Context Budget
|
||
|
||
`backend/src/agent/app.py` MUST select tools with hybrid `get_tools_for_query(query, prefetch_available)` — keyword primary + embedding fallback.
|
||
|
||
| Intent | Tool subset (approx.) |
|
||
|--------|-----------------------|
|
||
| Capabilities/help | `show_capabilities` + any matched intents |
|
||
| Dashboard query with prefetch | `show_capabilities` + matched intents (search_dashboards suppressed) |
|
||
| Dashboard query without prefetch | `show_capabilities` + `search_dashboards` + matched intents |
|
||
| Maintenance | `show_capabilities`, `list_maintenance_events`, `start_maintenance`, `end_maintenance` |
|
||
| Unknown/general | `show_capabilities`, default set (search, health, env, task) |
|
||
|
||
Embedding fallback triggered when keyword matching yields <3 tools.
|
||
|
||
## 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
|