Back

Project 08

Agentic Research/Ops Assistant


A stateful multi-agent system on LangGraph: an Orchestrator with 3 tools, a Researcher sub-agent it can delegate multi-step work to, and a human-in-the-loop gate that a delegated sub-agent can't quietly route around.

The Approach

The Orchestrator runs a Planner-ToolExecutor-Planner ReAct loop until it has enough to answer. web_search always pauses for human approval via LangGraph's interrupt/resume mechanism, and a delegated Researcher sub-agent's own web_search calls go through the exact same gate, since it shares the same graph config: delegating a task doesn't bypass the approval a direct call would have needed.

The Results

Run against a real hosted LLM and real tools (not simulated), the CLEAR-framework eval reports 100% task completion and a 100% HITL-gate fire rate, but tool-call accuracy came out at 90%, not 100%, and stayed there: the one miss was the model answering trivial arithmetic directly instead of calling the calculator, reasonable model behavior, still counted as a miss under the eval's own rule, reported rather than graded away by softening the criteria.

Getting the deployed agent under a 512MB free-tier limit took two real, measured rounds. Dropping the RAG tool still left the container at 505.5MB/512MB under a real request/resume cycle, root-caused by docker exec-ing into the running container to a dependency's own optional import path pulling in torch/transformers regardless of whether this project's code used them. A hand-curated deploy requirements file cut that to 145.6MB (28%), stable across repeated requests, and dropped image size from 9.13GB to 648MB.

The Honest Parts

Every graph invocation gets one Langfuse trace, propagated via LangGraph's get_config() into every nested LLM/tool call, including a Researcher sub-agent's own internal spans nested under the same parent trace across an HITL pause/resume. Two real bugs a live user caught on the deployed demo, a Gradio streaming detection quirk and a JS wrapper that silently never ran, were fixed after being traced through the actual failing path, not re-derived from re-reading the code.

What I Learned

  • Delegation should never quietly bypass a safety gateA sub-agent sharing the same graph config as its parent means its own tool calls go through the identical HITL interrupt, so 'the Researcher decided to search' can't become a way to skip the approval a direct Orchestrator call would have required.
  • Root-cause memory by inspecting the running container, not by guessing from requirements.txtThe real memory cost came from a dependency's own optional import, invisible from reading this project's code alone. docker exec-ing in and checking sys.modules directly is what found it; a hand-curated deploy requirements file was the fix once the actual cause was known.
  • A live user will exercise a path no test doesBoth real bugs caught on the deployed demo, a generator-function wiring quirk in Gradio's click handler and a JS injection format mismatch, passed every prior test because those tests called the underlying function directly in Python, never through the actual UI wiring the bug lived in.
  • 90% reported honestly is worth more than 100% massagedThe one CLEAR-eval miss was defensible model behavior, not a bug, but it still failed the eval's own stated rule for what counts as a pass. Keeping the number as measured, and explaining the miss, is the same honest-reporting standard applied across every project in this portfolio.

Tech Stack

LangGraphLangChainLangfuseFastAPIGradioW&BDocker

Links