← AI Engineering & Agentic AI
Multi-agent · deployed

Agentic ERP supply-chain copilot

A planner asks a supply question in plain English. The system does the arithmetic exactly rather than guessing at it, and stops for a manager on anything above $10,000.

14
Suppliers modeled
3-tier network, 9 components, 3 DCs
99%
Held-out intent accuracy
DSPy MIPROv2, 5-fold CV on 100 labeled queries (zero-shot: 91%)
168
Test functions
unit, integration, and red-team
$0
LLM API cost
GitHub Models free tier, keyword fallbacks

The problem

Supply-chain planners ask questions in sentences. Optimization solvers read constraint matrices. Put a language model between the two and let it do the math, and it returns plausible answers that cost money when they're wrong. A routing plan that looks optimal and isn't shows up later as a real invoice.

Architecture

A LangGraph StateGraph routes each query through 6 nodes. The intent classifier maps natural language to 1 of 10 bounded intents (mcnf_solve, jsp_schedule, vrp_route, robust_allocate, meio_optimize, bullwhip_analyze, disruption_resource, kg_query, contract_query, multi_step). A DSPy-compiled classifier (MIPROv2) takes over from the zero-shot baseline at runtime when its JSON artifact is present, and the zero-shot path stays available with no config change.

The Think-on-Graph agent extracts entities, selects relations from a whitelist, and traverses Neo4j with parameterized Cypher. The CRAG stack (BGE-large-en-v1.5 dense search, BM25 sparse search, RRF at k=60, and a CrossEncoder reranker) retrieves contract chunks and labels each one Correct, Ambiguous, or Incorrect before the synthesizer sees it. A two-level Redis semantic cache skips the LangGraph traversal entirely on a repeat query: SHA-256 exact match first, then cosine similarity at 0.95 with a discriminating-token guard for numbers and entity codes.

When a solver result exceeds $10,000, the graph pauses at a human-gate node via LangGraph interrupt(). The decision UUID sits in Redis with a 24-hour TTL. The approve endpoint requires the manager password, resumes the graph with the decision, and guards against a double submission (HTTP 409 on re-submission). In the browser, the planner sees a warning banner with Approve and Reject buttons that resolve into a green or red outcome badge.

Stack

Layer Tool Why
Orchestration LangGraph StateGraph 6 nodes: classify → {kg_agent | contract_agent | solver_dispatch} → human_gate (conditional) → synthesize, with a conditional edge (route_by_intent) choosing the branch. A MemorySaver checkpointer holds the paused run across the human-in-the-loop interrupt.
Intent classification Zero-shot with DSPy opt-in The primary path is a structured-output call with 10 bounded intents and a confidence threshold. The optional upgrade is a DSPy MIPROv2-compiled ChainOfThought loaded from a JSON artifact, and the zero-shot path stays in place when DSPy is missing or the artifact is absent.
Retrieval (CRAG) BGE-large-en-v1.5, BM25, RRF 1024-dim dense search via pgvector ivfflat and BM25Okapi sparse search, fused with RRF (k=60), then a CrossEncoder reranker and an LLM relevance gate that labels each passage Correct, Ambiguous, or Incorrect.
Knowledge graph Neo4j 5, whitelisted Cypher Think-on-Graph: extract entities, select relations from a whitelist, traverse with 1 retry that self-corrects on an empty subgraph. No raw model text reaches a database query.
Solvers OR-Tools, CVXPY, SciPy SLSQP 7 exact solvers: MCNF (LP), VRP (CVRP), JSP, robust min-max (CVXPY), MEIO GSM (SLSQP, non-convex), bullwhip analysis, and disruption resourcing. One solver per intent, and the language model does none of the math.
MCP contracts 6 FastMCP servers server_erp, server_kg, server_crag, server_ortools, server_cvxpy, server_scipy. They expose the solvers and data stores to any MCP client as typed tools. Inside the graph, solver_dispatch validates the parameters the model extracts against a Pydantic input model per solver (SolveMcnfInput and 6 more) before anything runs.
Semantic cache Redis with BGE cosine (0.95) Two-level lookup: SHA-256 exact match, then cosine similarity. A discriminating-token guard (number multiset plus entity code set) blocks false cache hits when 2 queries share an embedding but differ in their parameters.
Security Manager password on approvals The approve endpoint answers 403 to a wrong manager password and 503 while none is set. JWT auth (HS256, 60-minute expiry), a 3-role RBAC map, and a regex PII scrubber (emails, phone numbers, US Social Security numbers) are written and tested but not yet wired into the API routes.
CI / CD 6-job GitHub Actions pipeline ruff, black, mypy, bandit, and pytest unit tests → frontend tsc → integration tests → red-team (promptfoo and pytest) → build and push to ACR → repository_dispatch to the deployment repo.
Fine-tune DPO and QLoRA on Llama 3.1-8B Scripts turn LangSmith traces into preference pairs and train with LoRA r=16 on all projection layers. Written, not yet run: training waits for 5,000 preference pairs and a Lightning AI L4 GPU.

What I learned

The hardest boundary to enforce sits where model output turns into executable code, well before the agent graph. Once every solver input had to pass a typed Pydantic model and every Cypher query used whitelisted relations with bound parameters, whole classes of failure stopped appearing. The model can still misread a request and pick the wrong tool, which a person catches. It can't form a malformed query.

The discriminating-token guard on the semantic cache was the fix I didn't expect to need. BGE embeddings wash out the difference between "allocate 400 units" and "allocate 1000 units": cosine similarity clears 0.95, and the correct answers are nothing alike. The guard compares the numbers and entity codes in both queries and refuses the cached response when they differ.

  • LangGraph
  • FastMCP
  • DSPy
  • CRAG
  • BGE-large-en-v1.5
  • Neo4j
  • pgvector
  • OR-Tools
  • CVXPY
  • SciPy SLSQP
  • Redis
  • FastAPI
  • React/Vite