Status: a pilot / prototype built in October 2026. It has run locally, not in production. Everything below describes what the code does today, and says so where a part is only designed.
The problem
Ops messages at VNTRIP arrive through many channels, and the expensive failure is not a wrong answer — it is the message nobody picked up. An assistant that guesses is worse than none, so the core had to be built around knowing when not to answer.
Intake: nine channels, one event, exactly once
Nine inbound channels — Telegram, email over IMAP, Zalo OA (customer and internal), Chatwoot (including personal Zalo), PBX missed calls and recordings, TMS chat, Copilot and Mattermost — go through n8n workflows that normalise them into one event shape.
The backend intake is idempotent: claim the event, then
INSERT … ON CONFLICT DO NOTHING plus FOR UPDATE SKIP LOCKED so two workers
never process the same event, at most 3 attempts, then complete. A channel
that retries its webhook cannot create a second ticket.
Routing: a tiered classifier, exactly one agent per turn
Intent is classified in tiers, cheapest first, and a tier only answers when it clears its confidence threshold:
| Tier | What it is | Threshold |
|---|---|---|
| Rules | 17 intents, 135 phrases on diacritic-stripped text, entity rules for order refs and PNRs | 0.85 |
| kNN | nearest labelled examples — planned | 0.85 |
| Small LLM | a cheap model for what rules cannot place | 0.7 |
| Strong LLM | the last resort | 0.5 |
The output is exactly one agent per turn, and that is enforced by the types, not by convention — there is no code path that fans a turn out to two agents.
Agents under an autonomy ladder
There are 10 domain agents. Flight, hotel and knowledge are live in the pilot; post-booking, finance, corporate, dispatch, quality, proactive and sourcing are designed but not built.
Every action sits on an autonomy tier: T0 read, T1 internal write, T2 external send, T3 money or seats. The pilot executes only T0 and T1. Anything at T2 or T3 always hands off to a human.
Guardrails
- PII vault redaction on every LLM, embedding and rerank call — personal data is swapped out before any model sees the text.
- Six restricted topics — visa, immigration, legal, compensation, refund amount and VAT invoice — trigger an immediate human handover.
- Prompt-injection attempts are flagged.
- Knowledge answers are cited sentence by sentence, with a check for unverified claims; an answer that cannot be grounded is refused. How that works is in the RAG write-up.
Tickets, SLA and realtime
A ticket state machine with 9 statuses, 31 transitions and 19 ticket types.
SLA runs on a business-hours engine over a Vietnamese holiday calendar — 8
clocks, a 30-second scanner using SKIP LOCKED, and alerts to Telegram and
Mattermost sent through an outbox.
Socket.IO realtime events are emitted only after commit, so a screen never shows a state the database rolled back. Every interaction is kept in an append-only, redacted log.
The numbers
About 50k lines of TypeScript, about 5.4k lines of hand-written SQL, and 1,182 unit tests.
In a local pilot run with the LLM switched off, the knowledge path answered 99 questions from the knowledge base with citations, at a p50 of roughly 1.9–2.4 s, and gave an honest "no source" on 33 more — the behaviour the whole design is built to protect.