A production-style support triage system built with AgentField, Python, React, and Docker Compose. Routing uses a local Ollama model. A small System One decision gate can skip the two chat calls on easy tickets.
Your Browser
│
▼
┌──────────────────┐ port 3000
│ React UI │ ◄── nginx serves static files
│ (nginx) │ /api/* proxied to control plane
└────────┬─────────┘
│ /api/v1/execute/triage-orchestrator.handle_ticket
▼
┌──────────────────────────────────────────┐ port 8080
│ AgentField Control Plane │
│ Routing · Memory · DAG · Policy · Audit│
└────────┬─────────────────────────────────┘
│ (agentfield-net)
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ ┌────────────┐
│orchestr. │ │ gate │ │sentiment │ │ sla │ │ escalation │
│ port 9001│ │ port 9005│ │ port 9002│ │ :9003 │ │ port 9004 │
│ Reasoner │ │ Skill │ │ Reasoner │ │ Skill │ │ Reasoner │
│ │ │ systemone│ │ Ollama │ │ rules │ │ Ollama │
└──────────┘ └────┬─────┘ └──────────┘ └───────┘ └────────────┘
│
▼
Ollama on the host :11434
POST /v1/systemone (tev1:0.8b)
POST /api/chat (gemma4:31b-cloud)
Submit ticket
│
▼
triage-orchestrator.handle_ticket
│
├── asyncio.gather ──────────────────────────┐
│ │
▼ ▼
gate-agent.decide sla-agent.get_policy
POST /v1/systemone (pure Python rules)
team, urgency, threat, sla_minutes, boost
escalation, frustration
│
├─ confident, not critical, not an escalation candidate
│ │
│ ▼
│ FAST PATH
│ team + one-line summary from the gate
│ sentiment synthesized from gate answers
│ sentiment-agent and app.ai() are not called
│
└─ low confidence, error, critical, or escalation candidate
│
▼
FULL PATH (unchanged)
sentiment-agent.analyze
orchestrator app.ai() routing
│
escalate=true?
│
▼
escalation-agent.create_case
│
▼
Result: same fields as before, plus
decision_path, gate, gate_latency_ms
The gate is an optimization. It is never the only authority. A low score, a transport error, a missing model, critical urgency, needs_escalation, or an enterprise account already marked high urgency all take the full path. Escalation is created only on that full path.
gate-agent is a Skill. It does not call app.ai(). It sends one raw HTTP request to Ollama:
POST ${SYSTEMONE_BASE}/v1/systemone
model: ${SYSTEMONE_MODEL} # default tev1:0.8b
The live call times out after SYSTEMONE_TIMEOUT_S (default 3 seconds) and returns {"ok": false, "error": "..."} instead of raising. The orchestrator then runs sentiment and routing as before. The first call after a cold start can exceed that timeout while Ollama loads tev1:0.8b; that ticket takes the full path, and later tickets can take the fast path.
tev1:0.8b is experimental, with about a 2,000-token context, and the state is included again for every question. Ticket text (subject + body) is truncated to 1,200 characters and the question text is kept short.
Confidence is distribution concentration (1 - H/ln N), not a calibrated probability of being correct. On a 42-ticket local run, tev1:0.8b kept the minimum under 0.7, so every ticket took the full path. That is the intended safety behavior. Lower GATE_CONFIDENCE_MIN only after you measure field accuracy with eval/run_eval.py.
Checked on 2026-10-02 against the System One API and the decision guide. The request shape in older notes does not match that API. This repo follows the published schema:
| Question | Type | Criteria | Answer used |
|---|---|---|---|
team |
choice |
object, 4 team keys | choice |
urgency |
choice |
low / medium / high / critical |
choice |
contains_threat |
noul |
object with false and true strings |
probability noul |
needs_escalation |
noul |
object with false and true strings |
probability noul |
frustration |
score |
array of 10 descriptions, low to high | weighted score |
Assumptions on top of that schema:
- A noul answer is a probability, not a boolean.
>= 0.5becomes true. - Noul answers have no
confidence. The gate usesabs(p - 0.5) * 2(0 at a coin flip, 1 at 0 or 1) and then takes the minimum across every answer. scoreis a weighted index from 0 through 9, not a 1–10 rating. Index 0 maps to frustration 1 and index 9 maps to frustration 10.- Fast path also requires the ticket not be enterprise + high urgency, because that case is already an escalation in the routing prompt.
- There is no Ollama CLI or client library for this endpoint. The gate uses
httpx.
Inside Docker the base URL is http://host.docker.internal:11434, not localhost.
- Docker Desktop (or Docker Engine + Compose v2)
- Ollama 0.35 or newer, reachable on port 11434
- The chat model the agents already use, plus the decision-gate model:
ollama pull gemma4:31b-cloud
make pull-gate-model # ollama pull tev1:0.8btev1:0.8b is the default gate. Set SYSTEMONE_MODEL to swap it. nimble is another local System One model and can be compared with the eval script.
git clone https://github.com/AkhileshSM/support-system.git
cd support-system
make setup # copies .env.example → .env.env is gitignored. Commit .env.example only. The example file has no secrets.
make upThis builds and starts 7 containers:
agentfield-server— control planeorchestrator— triage agent (fast path or full path)gate-agent— Ollama/v1/systemonedecision gatesentiment-agent— sentiment and urgency, full path onlysla-agent— deterministic SLA lookupescalation-agent— escalation casesui— React UI served by nginx
| Service | URL |
|---|---|
| React UI | http://localhost:3000 |
| AgentField dashboard | http://localhost:8080 |
The pipeline trace shows a Gate stage. On a fast-path ticket, Sentiment and AI Routing are marked skipped. The result panel shows decision_path, gate confidence, and gate latency.
make logs
make ui-dev # React on http://localhost:5173
make rebuild SVC=gate-agent
make down
make cleaneval/tickets.py holds 42 labeled tickets, including the four UI presets. Labels are team, urgency, and escalate (critical, or enterprise and high).
# Host Ollama, not the Docker hostname
SYSTEMONE_BASE=http://127.0.0.1:11434 \
SYSTEMONE_MODEL=tev1:0.8b \
python eval/run_eval.py
# Optional third column
SYSTEMONE_COMPARE_MODEL=nimble python eval/run_eval.py
# Dataset only, no model calls
python eval/run_eval.py --checkThe script prints accuracy per field, the fast-path rate, and p50/p95 latency. SYSTEMONE_MODEL, BASELINE_MODEL, and SYSTEMONE_COMPARE_MODEL come from the environment. The existing path is the two production chat prompts (sentiment, then routing), not a second trip through the orchestrator, so the gate is not mixed into column (a).
support-system/
├── docker-compose.yml # 7-service stack
├── Dockerfile.server # AgentField control plane
├── Makefile # make up / down / logs / pull-gate-model
├── .env.example # copy → .env (gitignored)
├── nginx/
│ └── nginx.conf
├── eval/
│ ├── tickets.py # 42 labeled tickets, including UI presets
│ └── run_eval.py # existing path vs systemone gate
├── agents/
│ ├── orchestrator/
│ │ ├── app.py # Fast path or full sentiment + routing
│ │ ├── startup.py
│ │ ├── requirements.txt
│ │ └── Dockerfile
│ ├── gate/
│ │ ├── app.py # Skill: POST /v1/systemone
│ │ ├── systemone.py # Request builder and normalizer
│ │ ├── startup.py
│ │ ├── requirements.txt
│ │ └── Dockerfile
│ ├── sentiment/
│ ├── sla/
│ └── escalation/
└── ui/
└── src/
├── components/
│ ├── PipelineTrace.jsx # Gate stage; sentiment/routing skipped on fast path
│ ├── ResultPanel.jsx # decision_path, gate confidence, latency
│ └── AgentStatusBar.jsx # includes gate-agent
└── hooks/
└── useAgents.js
All via the control plane at http://localhost:8080:
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/execute/triage-orchestrator.handle_ticket |
Triage pipeline |
| POST | /api/v1/execute/gate-agent.decide |
Decision gate only |
| POST | /api/v1/execute/sentiment-agent.analyze |
Sentiment only |
| POST | /api/v1/execute/sla-agent.get_policy |
SLA lookup only |
| POST | /api/v1/execute/escalation-agent.list_cases |
List escalation cases |
| GET | /api/v1/executions |
Execution history |
| GET | /api/v1/agents |
Registered agents |
| GET | /health |
Control plane health |
curl http://localhost:8080/api/v1/execute/triage-orchestrator.handle_ticket \
-H "Content-Type: application/json" \
-d '{
"input": {
"ticket_id": "T-001",
"subject": "All production data gone - URGENT",
"body": "We cannot access our database since this morning. Complete outage.",
"customer_id": "CUST-ACME",
"account_tier": "enterprise"
}
}'The result still has ticket_id, team, escalated, confidence, summary, sla_minutes, priority_boost, sentiment (label, urgency, frustration_level, key_emotion, contains_threat), escalation, and processed_at. It also has:
| Field | Meaning |
|---|---|
decision_path |
"fast" or "full" |
gate |
Normalized gate dict, or {"ok": false, "error": "..."} |
gate_latency_ms |
System One call time, when the gate returned one |
A clear low-urgency ticket comes back with decision_path: "fast". The data-loss ticket above is critical, so it takes decision_path: "full" and can still escalate. If Ollama is down or tev1:0.8b is not pulled, the gate returns ok: false and the orchestrator runs the full path instead of failing the request on the gate call. The full path itself still needs the chat model.
| Variable | Default | Used by |
|---|---|---|
OLLAMA_API_BASE |
http://host.docker.internal:11434 |
Chat agents |
SYSTEMONE_BASE |
http://host.docker.internal:11434 |
gate-agent |
SYSTEMONE_MODEL |
tev1:0.8b |
gate-agent and eval/run_eval.py |
GATE_CONFIDENCE_MIN |
0.7 |
Orchestrator fast-path threshold |
SYSTEMONE_TIMEOUT_S |
3 |
Gate HTTP timeout |
BASELINE_MODEL |
gemma4:31b-cloud |
Eval script, existing path |
SYSTEMONE_COMPARE_MODEL |
unset | Eval script, optional third column |