A portfolio writeup of the deep-agent architecture I designed and shipped for the AMIQ platform (Ads & Marketing IQ). AMIQ is Sobhan Bahrami's agentic burn-detection and campaign-analytics platform. This repo documents the deep agent layer — ReAct, Plan-and-Execute, the Strategy Agent with human-in-the-loop, and the pending-actions API — that I added to it.
⚠️ All code lives in the upstream AMIQ repo. This repo is a writeup, not a fork. The point is to explain the architecture and the design decisions, with pointers to the source files.
In May 2026 I shipped a 3,693-line deep-agent layer on top of AMIQ's existing audit pipeline. The addition:
- Two agent architectures — ReAct (single-agent, multi-turn tool use) and Plan-and-Execute (decompose → execute → reduce)
- A Strategy Agent with confidence-gated action generation
- A human-in-the-loop pending-actions API — every strategy proposal is stored, surfaced, and requires explicit human approval before any writeback to Meta/Google APIs
- A streaming protocol so the dashboard can show LLM tokens, tool calls, and tool observations in real time
All built on LangChain 1.2.x's create_agent API (which returns a
CompiledStateGraph directly — no AgentExecutor needed) and AMIQ's
existing FastAPI + Supabase + LangGraph stack.
┌─────────────────────────────┐
│ AMIQ core audit pipeline │
│ (Sobhan's original work) │
│ │
│ INGEST → AUDIT → REPORT │
│ (LangGraph state) │
└─────────────┬───────────────┘
│ audit_results
▼
┌─────────────────────────────┐
│ Strategy Agent ◄────┐ │
│ (this repo) LLM │
│ │ │
│ decision_confidence ─┘ │
│ < 0.50 → DATA_FIX only │
│ >= 0.50 → full actions │
└─────────────┬───────────────┘
│ N proposals
▼
┌─────────────────────────────┐
│ pending_actions table │
│ (Supabase, RLS-protected) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ Dashboard (Next.js) │
│ "Review N actions" card │
└─────────────┬───────────────┘
│ human PATCHes status
▼
┌─────────────────────────────┐
│ APPROVED / REJECTED │
│ (logged; Phase 4+ writes │
│ back to Meta/Google APIs)│
└─────────────────────────────┘
────────────────────────────────────────────────────
┌─────────────────┐ ┌──────────────────┐
│ ReAct Agent │ │ Plan-and-Execute│
│ (deep_agents/ │ │ (deep_agents/ │
│ react_agent) │ │ plan_execute) │
└────────┬────────┘ └────────┬─────────┘
│ same tool layer │
└────────────┬──────────┘
▼
┌─────────────────────────┐
│ deep_agents/tools.py │
│ 9 tools, 502 lines: │
│ query_campaigns │
│ query_metrics │
│ query_audit_results │
│ query_reports │
│ get_company_settings │
│ calculate_burn_score │
│ compare_campaigns │
│ generate_burn_alert │
└─────────────────────────┘
Multi-turn tool-use reasoning via LangChain 1.2.x's create_agent
which returns a CompiledStateGraph directly runnable with
.stream() / .invoke() — no AgentExecutor needed.
from src.deep_agents.react_agent import build_react_agent
agent = build_react_agent(company_id=uuid, plan="starter")
result = await agent.ainvoke({"messages": [{"role": "user", "content": "..."}]})
# result["messages"] contains the full conversation including tool callsStreaming protocol yields {"type": "token"|"tool"|"obs"|"final", "content": ...}
events so the dashboard can render LLM output in real time.
Design choice: I deliberately did not roll my own ReAct loop.
LangChain 1.2.x's create_agent is the canonical ReAct implementation
and is what every team will recognize. The value of this file is the
streaming protocol and the integration with AMIQ's LLM router
(src.llm.router.get_llm), not a custom agent loop.
Three sub-agents:
- Planner — single LLM call that emits a structured JSON plan
- Executor — a ReAct-style agent that runs each step with tools
- Reducer — single LLM call that synthesizes the step results
The pattern is the classic Wang et al. 2023 "Plan-and-Execute" but implemented in <400 lines by reusing the ReAct agent for the executor sub-agent.
When to use which:
- ReAct for single, focused investigations ("which campaigns are burning the most this week?")
- Plan-and-Execute for multi-step analyses ("compare Q1 vs Q2 across all channels, identify the worst 3 campaigns, and propose reallocations")
Nine tools, each one a thin wrapper around AMIQ's existing services:
| Tool | What it returns |
|---|---|
query_campaigns(company_id) |
List of campaigns with status, channel, spend |
query_campaign_metrics(company_id, date_range, ...) |
Daily metrics with filters |
query_audit_results(company_id, run_id) |
Latest audit findings |
query_reports(company_id) |
Historical natural-language reports |
get_company_settings(company_id) |
Account config, integrations, plan |
calculate_burn_score(company_id, ...) |
Burn score (0–100) with breakdown |
compare_campaigns(company_id, campaign_ids) |
Side-by-side metric comparison |
generate_burn_alert(company_id, severity, ...) |
Insert a Slack/email alert |
_interpret_score(score, severity, details) |
Score → human language |
Design choice: every tool takes company_id as a string and
validates RLS context internally — agents cannot accidentally query
across companies. The tool layer is the trust boundary.
This is the part I'm most proud of. The Strategy Agent:
- Reads audit results
- Calls the deep ReAct agent with the audit context
- Proposes N strategy actions (e.g.,
PAUSE_CAMPAIGN,BUDGET_SHIFT) - Confidence-gated:
- If
decision_confidence < 0.50→ onlyDATA_FIXactions - If
>= 0.50→ full strategy actions
- If
- Writes each proposal to the
pending_actionstable withstatus=PENDING
The human-in-the-loop contract:
Strategy Agent → pending_actions table (status=pending)
↓
Dashboard surfaces "Review N actions"
↓
User PATCHes /pending-actions/{id} → approve or reject
↓
status=approved|rejected (logged)
↓
Phase 4+ → write back to Meta/Google APIs
Why the gate at 0.50: Below that, the agent doesn't have enough
signal to propose real changes; instead it should be raising tracking
issues (DATA_FIX) so that future audits are more reliable. The
"refuse to act below 0.50" rule is the single most important piece
of operational discipline in the whole system.
Five endpoints:
| Method | Path | What |
|---|---|---|
| POST | /pending-actions/generate |
Trigger Strategy Agent, return proposed actions |
| GET | /pending-actions/ |
List pending actions for the current company |
| GET | /pending-actions/{id} |
Get one action's full context |
| PATCH | /pending-actions/{id} |
Approve or reject |
| GET | /pending-actions/stream |
SSE stream of strategy generation events |
Pydantic-validated request/response, Supabase RLS on every query, full
audit trail in the pending_actions table.
- No writebacks to Meta/Google APIs in Phase 3. Every approved
action is logged with
status=APPROVEDbut nothing is pushed. The system proposes; the human disposes. The writeback layer is the next phase and needs a separate round of safety review. - No custom agent loop. I used LangChain 1.2.x's
create_agentrather than rolling my own. The point of this layer is the AMIQ- specific orchestration, not re-implementing ReAct. - No fine-tuning. Routing is by
plan(starter/pro/enterprise), not by model selection at the agent level. Fine-tuning is a 2027 problem, not a 2026 one.
- Confidence gating is non-negotiable. Without the 0.50 gate, the agent will propose plausible-looking actions on thin evidence. With the gate, it correctly refuses to act on weak audits and instead raises data-quality issues.
- The tool layer is the trust boundary. Every tool validates RLS context, every tool returns enough context to be auditable. Tools are the API; the agent is just a router over them.
- HITL is the right default for every write action. "Just approve it later" means "approvals are skipped". The pending- actions table is the only way to make review the path of least resistance.
- Streaming the LLM tokens is a UX feature, not a perf feature. Users tolerate long agent runs only if they can see what's happening. Without the SSE stream, "the agent is thinking" for 30 seconds looks like a hang.
- Don't over-engineer the tool layer. Nine tools is enough.
Resist the urge to add
query_everything_as_json— it encourages the agent to dump data instead of using focused tools.
All in the live repo at
sobhanb-eth/ads-marketing-iq:
| File | Lines | What |
|---|---|---|
src/deep_agents/__init__.py |
22 | Public exports |
src/deep_agents/react_agent.py |
344 | ReAct single-agent + streaming |
src/deep_agents/plan_execute.py |
380 | Plan-and-Execute three-stage agent |
src/deep_agents/tools.py |
502 | 9 tools with RLS-safe context |
src/agents/strategy.py |
376 | Strategy Agent + confidence gating |
src/api/routes/pending_actions.py |
323 | 5 REST endpoints for the HITL flow |
src/models/pending_action.py |
127 | Pydantic models + 8 ActionTypes + 5 statuses |
supabase/migrations/003_deep_agent_layer.sql |
204 | pending_actions table, RLS, indexes |
Total: 2,278 lines of agent code + 1,415 lines of supporting
infrastructure (anomaly scoring, memory service, routing cache, DB
migration) = 3,693 insertions in the single feat(amiq): deep agent layer commit.
| Layer | Choice |
|---|---|
| Agent framework | LangChain 1.2.x create_agent (returns CompiledStateGraph) |
| LLM routing | AMIQ's src.llm.router — Qwen 3.5-9B local (Ollama) → MiniMax M2.7 cloud → OpenRouter fallback |
| LLM tool-calling schema | LangChain @tool decorator on each tools.py function |
| Streaming | Server-Sent Events (SSE) on the FastAPI endpoint, AsyncGenerator on the client |
| State | Supabase PostgreSQL + Row-Level Security (RLS) |
| Validation | Pydantic v2 everywhere |
| Logging | Structured logger.info(..., key=val) style — no f-strings in hot paths |
- Make confidence gating tunable per-company. Hard-coded 0.50 is
fine for the first 5 customers, but a company with $10M/month spend
should probably have a 0.70 gate. A setting on the
companiestable. - Add a "show me the plan" preview. Currently the agent goes straight to action proposals; surfacing the ReAct thought process before the action list would make review faster.
- Per-tool rate limits. Right now a single agent run can hammer
query_campaign_metrics30 times. A per-tool rate limit per audit run would cap the worst case.
This writeup is MIT. The code it describes is proprietary to AMIQ
AI and lives upstream at
sobhanb-eth/ads-marketing-iq.
The architecture described here was designed and shipped by Pouya
Zargar in May 2026, on top of the original AMIQ platform by Sobhan
Bahrami.