75 lines
6.4 KiB
Markdown
75 lines
6.4 KiB
Markdown
#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 static `interrupt_before` for HITL and checkpointed resume keyed by `thread_id=conversation_id`. Current implementation recreates the graph with `interrupt_before=[]` on confirm/deny resume to prevent pausing again before the same tool.
|
|
**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 dual-identity headers (`Authorization: Bearer <service_jwt>` + `X-User-JWT: <user_jwt>`).
|
|
**Rationale**: Single source of metadata. Auto OpenAI function schema. Old `@assistant_tool` → `@DEPRECATED`.
|
|
**Rejected**: Dual registration (`@assistant_tool` + `StructuredTool`) — redundant.
|
|
**Current fact (2026-06-30)**: `backend/src/agent/tools.py` contains 24 native LangChain tools plus `get_tools_for_query()` for hybrid intent-based subset selection. Agent runtime does not call the deprecated assistant registry.
|
|
|
|
## 3. Confirmation: `interrupt_before=DANGEROUS_TOOLS`
|
|
|
|
**Decision**: LangGraph native `interrupt_before=DANGEROUS_TOOLS`. No custom `HumanInTheLoopMiddleware` — LangGraph provides HITL natively via checkpointing. Second `submit()` with `additional_inputs[1]="confirm"/"deny"` triggers resume; the resumed graph uses `interrupt_before=[]`.
|
|
**Rationale**: Zero REST endpoints. LangGraph checkpoint ensures safe resume.
|
|
**Rejected**: REST confirmation endpoints + polling — more code, more latency. Custom middleware — LangGraph has native interrupt support.
|
|
|
|
## 4. History: PostgreSQL Persistence + Thread Checkpoints
|
|
|
|
**Current fact (2026-06-29)**: The Gradio handler ignores Gradio's `history` parameter. Conversation records are stored through `agent_conversations` / `agent_messages`, while LangGraph continuity uses PostgresSaver checkpoints keyed by `thread_id=conversation_id`. `RunnableWithMessageHistory` is not a runtime requirement.
|
|
|
|
**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`, `examples`. All used. Concurrency handled by per-user in-memory lock + multi-tab REST gate; no global `concurrency_limit` 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 dual-identity headers: service JWT authenticates the agent, user JWT authorizes the operation. Implementation uses `_dual_auth_headers()` → `Authorization: Bearer {service_jwt}` + `X-User-JWT: {user_jwt}`.
|
|
**Rationale**: RBAC enforced by FastAPI under user identity. Service JWT provides agent-to-FastAPI trust; user JWT provides per-operation authorization.
|
|
**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.
|
|
|
|
## 9. Hybrid Tool Router: Keyword Primary + Embedding Fallback
|
|
|
|
**Decision**: Two-tier tool selection. Primary: word-boundary-aware keyword matching (regex `\b` guards for `tool`, `env`, `доступ`; `show_capabilities` early return removed). Fallback (keyword <3 tools): embedding-based cosine similarity between user query and tool description vectors via `paraphrase-multilingual-MiniLM-L12-v2` (420MB, 50+ languages). Top-K above similarity threshold 0.65 merged with keyword results.
|
|
**Rationale**: Pure substring matching caused P0 (tool⊂tools blocks all tools) and cannot handle synonyms/typos. Full catalog causes Tool Paralysis in small models. Embedding-only is blind to negations. Hybrid combines speed+determinism (keyword) with synonym/typo coverage (embedding).
|
|
**Rejected**: Full 24-tool catalog — causes Tool Paralysis in Gemma-level models.
|
|
**Rejected**: Embedding-only — adds 5-20ms to every request, blind to negations.
|
|
**Rejected**: Pure substring with cosmetic fixes — cannot handle "панели"≠"дашборды".
|
|
**Current fact (2026-06-30)**: `get_tools_for_query()` uses keyword primary with embedding fallback in `_embedding_router.py`. `fast_confirmation_tool()` uses negation guard.
|
|
|
|
## 10. Embedding Model Selection
|
|
|
|
**Decision**: `paraphrase-multilingual-MiniLM-L12-v2` (420MB) as default, configurable via `EMBEDDING_MODEL` env var. Lazy-loaded on first fallback call. Tool descriptions (RU+EN, 1-3 sentences each) pre-embedded at load time.
|
|
**Rationale**: Covers RU+EN (50+ languages), good quality/size ratio (MiniLM architecture, 384-dim vectors), works via `sentence-transformers` with ONNX runtime for CPU inference. Alternatives evaluated:
|
|
- `all-MiniLM-L6-v2` (80MB) — EN-only, no RU support. Rejected.
|
|
- `bge-small-en-v1.5` (130MB) — EN-only. Rejected.
|
|
- `intfloat/multilingual-e5-small` (470MB) — best quality but heavier. Kept as configurable alternative.
|
|
- `rubert-tiny2` (110MB) — RU-only, no EN. Rejected.
|
|
**Current fact (2026-06-30)**: Model not yet deployed; planned in Phase 1 implementation of hybrid router.
|
|
|
|
#endregion AgentChat.Research
|