Files
ss-tools/specs/033-gradio-agent-chat/research.md
2026-06-30 19:05:17 +03:00

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