6.4 KiB
#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