<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TorkNetwork</title>
    <description>The latest articles on DEV Community by TorkNetwork (@torkjacobs).</description>
    <link>https://dev.to/torkjacobs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3788387%2Fb6a33a8a-e444-421c-9a4c-9b6bc55aaddd.jpeg</url>
      <title>DEV Community: TorkNetwork</title>
      <link>https://dev.to/torkjacobs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/torkjacobs"/>
    <language>en</language>
    <item>
      <title>The 15-Point Checklist Before Deploying AI Customer-Facing</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:48:08 +0000</pubDate>
      <link>https://dev.to/torkjacobs/the-15-point-checklist-before-deploying-ai-customer-facing-3ic6</link>
      <guid>https://dev.to/torkjacobs/the-15-point-checklist-before-deploying-ai-customer-facing-3ic6</guid>
      <description>&lt;p&gt;You are about to put an AI system in front of your customers. Before you do, run through these 15 checks. Each one exists because someone, somewhere, shipped without it and paid the price.&lt;/p&gt;

&lt;p&gt;This is not theory. This is the list we use at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork&lt;/a&gt; before every customer deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security &amp;amp; Privacy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. PII Detection
&lt;/h3&gt;

&lt;p&gt;Can your system detect personally identifiable information in real-time — in the request path, before data is stored or forwarded to a third-party API?&lt;/p&gt;

&lt;p&gt;The minimum set: credit card numbers (Luhn validation), national ID numbers (format-specific per country), phone numbers, and email addresses. These are the data types that appear most frequently in customer conversations and carry the highest regulatory risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Send your chatbot a message containing a test credit card number (use 4111 1111 1111 1111 — the standard Luhn-valid test number). Check whether it appears in your conversation logs, your LLM provider's API logs, and your database. If it does, you do not have PII detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Data Isolation
&lt;/h3&gt;

&lt;p&gt;If your platform serves multiple customers or business units, is data separated between tenants?&lt;/p&gt;

&lt;p&gt;The test is specific: can Tenant A's knowledge base content, conversation history, or customer data appear in Tenant B's AI responses? This happens more often than vendors admit — shared vector databases without tenant-scoped queries are the usual cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Create two test tenants. Add a unique, fabricated fact to Tenant A's knowledge base (e.g., "Our company was founded on Mars in 1742"). Query Tenant B with a question that would surface this fact. If it appears, your data isolation is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Encryption
&lt;/h3&gt;

&lt;p&gt;TLS in transit. Encryption at rest. This is table stakes, not a feature.&lt;/p&gt;

&lt;p&gt;Every connection between the user's browser and your API should be TLS 1.2 or higher. Every database, cache, and object store should encrypt data at rest. Every API key, secret, and credential should be stored in a secrets manager, not in environment variables committed to version control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Run your API URL through an SSL checker. Review your database configuration for at-rest encryption settings. Search your repository for hardcoded API keys (&lt;code&gt;grep -r "sk-" .&lt;/code&gt; catches more than you would expect).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Access Control
&lt;/h3&gt;

&lt;p&gt;No open endpoints. Every API call should be authenticated. Every endpoint should be rate-limited.&lt;/p&gt;

&lt;p&gt;Authentication means API keys at minimum, OAuth or JWT for production. Rate limiting means per-session, per-tenant, and global limits. Without rate limiting, a single user — or a bot — can exhaust your LLM API budget in minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Call your chat endpoint without authentication headers. If you get a response instead of a 401, you have an open endpoint. Send 100 requests in 10 seconds from a single session. If all 100 succeed, you do not have rate limiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Data Residency
&lt;/h3&gt;

&lt;p&gt;Where is the data stored? Not where your server is — where the data physically resides. This includes your database, your cache, your LLM provider's API (which may log inputs), and any analytics or monitoring tools that ingest conversation data.&lt;/p&gt;

&lt;p&gt;POPIA requires that South African personal data be processed with appropriate safeguards. GDPR restricts data transfers outside the EU without adequate protection. CCPA gives California consumers rights over their data regardless of where the processor is located. The law that applies depends on where your customers are, not where your infrastructure is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Map every service that touches customer data. For each one, determine the data storage region. If you cannot answer "where is this data stored?" for every service in your stack, you are not ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  Governance &amp;amp; Compliance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6. Audit Trail
&lt;/h3&gt;

&lt;p&gt;Can you retrieve a complete, structured record of what your AI said to a specific customer at a specific time?&lt;/p&gt;

&lt;p&gt;A chat log is not an audit trail. An audit trail is queryable by customer, by conversation, by time range, and by action type. It includes the customer's input, what governance actions were taken (redaction, policy checks), what the AI received after processing, and what the AI responded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Pick a conversation from last week. How long does it take you to pull the complete interaction record — including any governance actions? If the answer is "I need to check multiple systems" or "I need engineering help," your audit trail has gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Compliance Receipts
&lt;/h3&gt;

&lt;p&gt;Does each interaction generate a signed, tamper-evident record that can be presented to a regulator as evidence?&lt;/p&gt;

&lt;p&gt;The difference between a log entry and a compliance receipt: a log entry says "the AI responded at 14:32." A compliance receipt says "at 14:32:07 UTC, the AI received input X (after PII redaction), generated response Y, which passed output policy check Z, and this record is signed with HMAC-SHA256 and has not been modified since creation."&lt;/p&gt;

&lt;p&gt;Under GDPR, data subjects can request a full accounting of how their data was processed. Under POPIA, a regulator can request evidence of appropriate safeguards. A signed receipt answers both requests. A log line does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Request a compliance receipt for a specific conversation from your platform. If the response is "we have logs," that is not the same thing. If the response is a structured record with a unique ID, timestamp, and cryptographic signature — you have receipts.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Policy Enforcement
&lt;/h3&gt;

&lt;p&gt;Can you define rules about what the AI can and cannot discuss, and are those rules enforced at the output level?&lt;/p&gt;

&lt;p&gt;A system prompt that says "do not discuss competitors" is a suggestion. The model may follow it. It may not. Policy enforcement means scanning the AI's output before it reaches the customer and blocking or flagging responses that violate defined rules.&lt;/p&gt;

&lt;p&gt;Topic boundaries, claim restrictions, required disclaimers, forbidden content categories — these should be code, not prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Add a policy rule that blocks a specific topic. Then ask the AI about that topic in five different ways — directly, indirectly, through hypotheticals, through comparison, and through a "just curious" framing. If any of the five gets through, your policy enforcement has gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Human Escalation
&lt;/h3&gt;

&lt;p&gt;When the AI cannot resolve a query, or when the customer is frustrated, is there an automatic path to a human?&lt;/p&gt;

&lt;p&gt;Automatic means the system detects escalation signals — explicit requests for a human ("speak to a manager"), frustration patterns (excessive capitalisation, repeated negative sentiment, insults), and repeated failed interactions (the customer asks the same question three times). Detection triggers a handoff without requiring the customer to find and click a button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Send your chatbot "I WANT TO SPEAK TO A REAL PERSON THIS IS ABSOLUTELY UNACCEPTABLE." If the AI responds with another AI-generated message instead of routing to a human, your escalation detection is not working.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Kill Switch
&lt;/h3&gt;

&lt;p&gt;Can you disable AI responses in under 5 seconds?&lt;/p&gt;

&lt;p&gt;Not "start a deployment." Not "merge a PR and wait for CI." A kill switch — one action that stops the AI from responding to customers. Per-tenant (disable one client), per-topic (disable a specific capability), or global (everything stops).&lt;/p&gt;

&lt;p&gt;When an AI starts generating harmful, incorrect, or embarrassing content at scale, the damage is measured in seconds. Your response time needs to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Time it. From the moment you decide to shut down, how many seconds until the AI stops responding to the next customer message? If it is more than 30 seconds, it is too slow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quality &amp;amp; Experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  11. Response Accuracy
&lt;/h3&gt;

&lt;p&gt;Have you tested with real customer questions — not synthetic benchmarks, not your own team's questions, but actual messages from actual customers?&lt;/p&gt;

&lt;p&gt;Build a test set of 50+ real customer queries (with answers verified by your team). Run them through the AI. Measure the accuracy rate. If it is below 90% for your domain, you need a better knowledge base, better prompts, or both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Collect the last 50 customer enquiries from your support inbox. Feed them to the AI. Have your team grade each response: correct, partially correct, or incorrect. Calculate the accuracy rate. Do this before launch, not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. Response Time
&lt;/h3&gt;

&lt;p&gt;Sub-3 seconds for the first visible token. Customers will not wait longer.&lt;/p&gt;

&lt;p&gt;This is not the time to generate the full response — it is the time until the customer sees the first word appearing on screen. SSE streaming makes this possible even when the full response takes 5-10 seconds to generate. Without streaming, the customer stares at a spinner and leaves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Measure time-to-first-token under realistic conditions — not on your local machine, but on the production infrastructure, with real network latency, during peak hours. If it is consistently above 3 seconds, either your model is too slow, your infrastructure is under-provisioned, or you are not streaming.&lt;/p&gt;

&lt;h3&gt;
  
  
  13. Fallback Behaviour
&lt;/h3&gt;

&lt;p&gt;What happens when the AI does not know the answer? There are two outcomes: it makes something up, or it says so honestly.&lt;/p&gt;

&lt;p&gt;The correct fallback is: "I don't have that specific information. Let me connect you with our team, or you can reach us at [contact details]." The incorrect fallback is a confident fabrication — an invented policy, a wrong price, a made-up feature.&lt;/p&gt;

&lt;p&gt;Hallucination is the default behaviour of language models. Honest fallback is a design decision that requires explicit instruction in the system prompt and validation in the output scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Ask the AI a question that is not in the knowledge base. Something specific and verifiable — a policy that does not exist, a product you do not sell, a location you do not operate in. If the AI invents an answer instead of acknowledging the gap, your fallback is not working.&lt;/p&gt;

&lt;h3&gt;
  
  
  14. Multi-Language
&lt;/h3&gt;

&lt;p&gt;If your customers speak multiple languages, does the AI detect the language and respond accordingly?&lt;/p&gt;

&lt;p&gt;Modern LLMs handle multilingual input natively — Claude, GPT-4, and Gemini all respond in the language of the input without explicit configuration. But your knowledge base may be in one language only. If a customer asks in Afrikaans and your knowledge base is in English, the RAG retrieval may fail because the embeddings do not match cross-lingually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Send the same question in every language your customers use. Check that the response is in the correct language and that the RAG retrieval returns relevant results. Cross-lingual RAG is a known weak point — if accuracy drops in non-primary languages, you may need multilingual embeddings or translated knowledge base content.&lt;/p&gt;

&lt;h3&gt;
  
  
  15. Monitoring
&lt;/h3&gt;

&lt;p&gt;Can you see conversations in real-time? Are you alerted when the AI escalates, when accuracy drops, or when anomalous patterns appear?&lt;/p&gt;

&lt;p&gt;Monitoring is not "we check the dashboard on Monday morning." It is automated alerts on: escalation rate exceeding a threshold, response time degradation, repeated unanswered questions (knowledge base gaps), and governance denials (potential abuse).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to test:&lt;/strong&gt; Trigger an escalation. How long until someone on your team knows about it? If the answer is "when they next check the dashboard," your monitoring is reactive, not proactive.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scorecard
&lt;/h2&gt;

&lt;p&gt;Count your checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15/15&lt;/strong&gt; — You are ready. Ship it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12-14&lt;/strong&gt; — You are close. The gaps are likely in monitoring, multi-language, or compliance receipts. These can be addressed post-launch if you have a plan and a timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8-11&lt;/strong&gt; — You have significant gaps. The missing items are probably in the governance section. Deploying without them is a calculated risk — make sure the people accepting that risk understand what they are accepting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Below 8&lt;/strong&gt; — You are not ready. The risk of a compliance incident, a customer data breach, or a reputational event is too high. Fix the foundations before launching.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing
&lt;/h2&gt;

&lt;p&gt;This checklist is designed to be platform-agnostic. You can use it to evaluate any AI chatbot, whether you built it yourself or bought it off the shelf.&lt;/p&gt;

&lt;p&gt;If you want a head start: &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; ships with items 1-10 enabled by default — PII detection, data isolation, encryption, access control, audit trails, compliance receipts, policy enforcement, escalation detection, and a kill switch. Items 11-15 depend on your specific deployment: your knowledge base quality, your infrastructure, your monitoring setup, and your customer base.&lt;/p&gt;

&lt;p&gt;Start free at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;. Read the full case for governed AI deployment in &lt;em&gt;The Agent Crisis&lt;/em&gt;, available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by the Tork team. Print the checklist. Check it before you ship. &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>checklist</category>
      <category>deployment</category>
      <category>governance</category>
    </item>
    <item>
      <title>I Compared 5 AI Chatbot Platforms on Governance — Here's What I Found</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:47:35 +0000</pubDate>
      <link>https://dev.to/torkjacobs/i-compared-5-ai-chatbot-platforms-on-governance-heres-what-i-found-59e8</link>
      <guid>https://dev.to/torkjacobs/i-compared-5-ai-chatbot-platforms-on-governance-heres-what-i-found-59e8</guid>
      <description>&lt;p&gt;Every AI chatbot comparison you have read compares features. Integrations, pricing tiers, UI polish, template libraries. These comparisons are useful if you are choosing a chatbot for a landing page.&lt;/p&gt;

&lt;p&gt;They are useless if you are choosing a chatbot that will interact with customers, handle personal data, and need to comply with data protection law.&lt;/p&gt;

&lt;p&gt;I compared five AI chatbot platforms on one dimension: governance. Not features. Not pricing. Governance — the ability to detect sensitive data, prove what the AI said, enforce policies, and hand off to humans when the AI is out of its depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The platforms
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tidio&lt;/strong&gt; — Popular with small businesses and e-commerce. AI chatbot powered by their Lyro product. Strong in automation and live chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chatbase&lt;/strong&gt; — Build a ChatGPT-style chatbot trained on your own data. Popular with developers and solo founders for quick deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intercom Fin&lt;/strong&gt; — Enterprise customer support AI. Part of the Intercom platform with deep CRM integration. Used by mid-market and enterprise teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshchat&lt;/strong&gt; — Part of the Freshworks suite. AI-powered customer messaging with Freddy AI. Common in mid-market support teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tork Chat&lt;/strong&gt; — Multi-agent AI assistant built governance-first. Full disclosure: this is our product. I will be as fair as possible, and you can verify the claims yourself at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The criteria
&lt;/h2&gt;

&lt;p&gt;I tested six governance capabilities. These are not nice-to-haves — they are the baseline for any AI system that handles customer data in a regulated environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. PII detection&lt;/strong&gt; — Does the platform detect personally identifiable information (credit card numbers, national ID numbers, phone numbers, email addresses) in customer messages before processing or storing them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Audit trail&lt;/strong&gt; — Can you retrieve a complete, structured record of what the AI said to a specific customer at a specific time? Not a chat log — a queryable audit record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compliance receipts&lt;/strong&gt; — Does each interaction generate a signed, tamper-evident receipt that can be presented to a regulator as proof of what occurred?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Escalation controls&lt;/strong&gt; — Can you define rules for when the AI should stop responding and hand off to a human? Not just a "talk to agent" button — automatic detection of frustration, confusion, or out-of-scope queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Data isolation&lt;/strong&gt; — If the platform serves multiple customers, is your data isolated from other tenants? Can one tenant's data leak into another tenant's AI responses?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Policy enforcement&lt;/strong&gt; — Can you define rules about what the AI can and cannot say? Topic restrictions, claim limitations, required disclaimers. Enforced at the output level, not just suggested in the system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform-by-platform results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tidio
&lt;/h3&gt;

&lt;p&gt;Tidio is a strong platform for small teams that need live chat with AI augmentation. Lyro, their AI agent, can be trained on your website content and FAQ documents, and it handles straightforward customer queries well.&lt;/p&gt;

&lt;p&gt;On governance, Tidio is limited. There is no PII detection — customer messages are processed and stored as-is. If a customer types their credit card number, it sits in the conversation log. Chat history is available through the dashboard, which serves as a basic log, but there are no structured audit records and no compliance receipts. Escalation is manual — the customer or the operator triggers a handoff. There is no automatic detection of frustration or out-of-scope queries. Tidio does offer workspace separation for teams, but there is no tenant-level data isolation in the way a multi-tenant SaaS requires. Policy enforcement is limited to what you put in the AI's training data and instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Small businesses that need a quick, affordable chatbot. Not suitable if you have compliance obligations for customer data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chatbase
&lt;/h3&gt;

&lt;p&gt;Chatbase makes it remarkably easy to deploy a custom chatbot. Upload your documents, connect your website, and you have a working bot in minutes. For developers and solo founders who need a fast deployment, it is hard to beat.&lt;/p&gt;

&lt;p&gt;Governance is minimal. There is no PII scanning — data flows through to the model as submitted. Conversation history is available and exportable, which is better than some alternatives, but there are no signed audit records. Escalation support is basic — you can configure keyword-based triggers to redirect to a human or a URL, but there is no sentiment analysis or frustration detection. Data is associated with your chatbot, but Chatbase does not offer the kind of cryptographic data isolation that regulated industries require. Policy enforcement relies on the system prompt — effective for broad instructions, but not enforceable at the output layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who need a quick, functional chatbot trained on custom data. Not suitable for customer-facing deployments with compliance requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intercom Fin
&lt;/h3&gt;

&lt;p&gt;Intercom Fin is the most mature platform in this comparison from a product perspective. It sits inside the Intercom ecosystem, which means deep integration with ticketing, CRM, and analytics. Fin is trained on your help centre content and resolves a significant percentage of support queries without human intervention.&lt;/p&gt;

&lt;p&gt;On governance, Fin is ahead of the other third-party platforms here. Intercom provides content filtering capabilities and the ability to restrict topics. Audit logs are available through the platform — conversation records are detailed and queryable. However, these are standard application logs, not cryptographic compliance receipts. Escalation is well-implemented — you can define routing rules based on conversation attributes, customer segments, and topic detection. Fin can recognise when it cannot resolve a query and hand off to a human agent with context. Data isolation is handled through Intercom's workspace architecture, which is robust for most use cases.&lt;/p&gt;

&lt;p&gt;The trade-off is cost and complexity. Fin is priced as an enterprise product and requires the broader Intercom platform. If you are already an Intercom customer, Fin is a strong choice. If you are evaluating standalone governance, the platform cost is significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mid-market and enterprise teams already on Intercom who need AI support with good escalation. Governance is partial — better than most, but not purpose-built for compliance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freshchat
&lt;/h3&gt;

&lt;p&gt;Freshchat, part of the Freshworks suite, offers AI-powered customer messaging through Freddy AI. It occupies a solid middle ground — more capable than the lightweight tools, more accessible than enterprise platforms.&lt;/p&gt;

&lt;p&gt;Freddy includes basic sentiment detection, which gives it some awareness of customer frustration. Standard conversation logging is available through the Freshworks platform. There are no compliance receipts — interactions are logged but not signed or independently verifiable. Escalation uses routing rules that can be configured based on keywords, topics, and basic sentiment signals. Data separation follows the Freshworks tenant model, which is adequate for most business use cases. Policy enforcement is limited to conversation design and bot configuration — there is no runtime output scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mid-market support teams already in the Freshworks ecosystem. Governance is basic but functional for low-regulation environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tork Chat
&lt;/h3&gt;

&lt;p&gt;Tork Chat was built governance-first. The governance layer is not an add-on — it is a node in the multi-agent state machine. Every message passes through it.&lt;/p&gt;

&lt;p&gt;PII detection runs in real-time on every input and every output. Credit card numbers (Luhn-validated), South African ID numbers (13-digit format), phone numbers, and email addresses are detected and redacted before the message reaches the LLM. The model never sees raw PII.&lt;/p&gt;

&lt;p&gt;Every governance scan generates an HMAC-signed audit receipt with a unique ID. Receipts record what was scanned, what was detected, what action was taken, and when. They are stored independently of the conversation and are queryable by conversation, tenant, or time range. These are not log entries — they are structured compliance records designed to be presented to a regulator.&lt;/p&gt;

&lt;p&gt;Escalation detection is automatic. Regex pattern matching catches explicit handoff requests ("speak to a manager"). A frustration classifier detects excessive capitalisation and negative sentiment patterns. When escalation triggers, the AI stops generating — a fixed handoff message is returned without an LLM in the loop.&lt;/p&gt;

&lt;p&gt;Data isolation is enforced at the tenant level. Each tenant has their own knowledge base (RAG scoped by tenant ID), session store, and bot configuration. Cross-tenant data leakage is architecturally prevented, not just policy-prevented.&lt;/p&gt;

&lt;p&gt;Policy enforcement operates at the output layer. The governance scan checks every AI response before it reaches the customer. Topic restrictions and claim limitations are enforced at runtime, not suggested in the system prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Customer-facing AI deployments where compliance, audit trails, and data protection are requirements — not features. Available to try free at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Tidio&lt;/th&gt;
&lt;th&gt;Chatbase&lt;/th&gt;
&lt;th&gt;Intercom Fin&lt;/th&gt;
&lt;th&gt;Freshchat&lt;/th&gt;
&lt;th&gt;Tork Chat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PII detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audit trail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic logs&lt;/td&gt;
&lt;td&gt;Chat history&lt;/td&gt;
&lt;td&gt;Detailed logs&lt;/td&gt;
&lt;td&gt;Standard logs&lt;/td&gt;
&lt;td&gt;HMAC-signed receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compliance receipts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Escalation controls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual only&lt;/td&gt;
&lt;td&gt;Keyword-based&lt;/td&gt;
&lt;td&gt;Rule-based&lt;/td&gt;
&lt;td&gt;Keyword + sentiment&lt;/td&gt;
&lt;td&gt;Auto-detect + pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workspace&lt;/td&gt;
&lt;td&gt;Per-chatbot&lt;/td&gt;
&lt;td&gt;Workspace&lt;/td&gt;
&lt;td&gt;Tenant model&lt;/td&gt;
&lt;td&gt;Tenant-scoped RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Policy enforcement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Training data&lt;/td&gt;
&lt;td&gt;System prompt&lt;/td&gt;
&lt;td&gt;Content filtering&lt;/td&gt;
&lt;td&gt;Bot config&lt;/td&gt;
&lt;td&gt;Runtime output scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To read this table: "Partial" means the capability exists in some form but does not meet the standard you would need for a compliance audit. "Basic logs" means conversation records exist but are not structured, signed, or independently queryable as audit evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for you
&lt;/h2&gt;

&lt;p&gt;The right choice depends on your context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your chatbot is internal-only&lt;/strong&gt; — answering employee questions, summarising documents, routing internal tickets — governance matters less. The risk profile is lower. Any of these platforms will work, and you should choose based on features, integrations, and price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your chatbot talks to customers&lt;/strong&gt; — answering enquiries, handling personal data, making statements that could be interpreted as commitments — governance is not optional. You need PII detection before a customer's ID number ends up in a log file. You need audit receipts before a regulator asks what your AI said. You need escalation rules before a frustrated customer gets three more paragraphs of AI-generated apology instead of a human.&lt;/p&gt;

&lt;p&gt;The question is not whether you need governance. If customers interact with your AI, you do. The question is whether you build it in now — when it is a design decision — or bolt it on later, when it is a remediation project triggered by an incident.&lt;/p&gt;

&lt;p&gt;Every platform in this comparison does something well. Tidio is fast and affordable. Chatbase is the quickest path from documents to chatbot. Intercom Fin has the deepest enterprise integration. Freshchat is a solid all-rounder in the Freshworks ecosystem. None of them were built with governance as the primary design constraint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; was. That is not a criticism of the other platforms — it is a statement about what we chose to prioritise. If governance is your priority too, evaluate it yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Evaluate Tork Chat free at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;. Read more about the case for governed AI agents in The Agent Crisis, available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatbot</category>
      <category>comparison</category>
      <category>governance</category>
    </item>
    <item>
      <title>How We Deployed AI Customer Service for a Vehicle Rental Company in 2 Weeks</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:47:02 +0000</pubDate>
      <link>https://dev.to/torkjacobs/how-we-deployed-ai-customer-service-for-a-vehicle-rental-company-in-2-weeks-1cb</link>
      <guid>https://dev.to/torkjacobs/how-we-deployed-ai-customer-service-for-a-vehicle-rental-company-in-2-weeks-1cb</guid>
      <description>&lt;p&gt;This is a case study from a real deployment of &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; for a vehicle rental company in South Africa. The numbers are honest estimates based on observed usage, not vanity metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client
&lt;/h2&gt;

&lt;p&gt;A mid-size vehicle rental operator based in Cape Town with 200+ vehicles across three locations. Their fleet ranges from economy hatchbacks to luxury SUVs, with a growing 4x4 and bakkie segment for tourists and contractors. They serve a mix of walk-in airport customers, online bookings, corporate accounts, and long-term leases.&lt;/p&gt;

&lt;p&gt;On a typical day, they handle 50+ customer enquiries — split roughly between WhatsApp, phone calls, email, and their website contact form. The enquiry mix is predictable: pricing and availability questions account for about 40%, booking and reservation requests about 25%, insurance and policy questions about 20%, and the remaining 15% is a mix of complaints, after-hours messages, and general questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The company had three staff members handling customer enquiries. Their day looked like this:&lt;/p&gt;

&lt;p&gt;The same ten questions, asked fifty times. "How much for an SUV for the weekend?" "Do you deliver to the airport?" "What's your fuel policy?" "Can I take the car across the border into Namibia?" "Is insurance included?" These questions have definitive answers that do not change from day to day. But each one required a human to read the message, find the answer, type a response, and move on to the next.&lt;/p&gt;

&lt;p&gt;After-hours enquiries went unanswered until the next morning. The rental industry does not sleep at 5pm. Tourists landing at 9pm, business travellers adjusting plans at midnight, conference organisers confirming fleet bookings on a Sunday — these enquiries sat in inboxes until Monday morning. By then, some customers had already booked with a competitor.&lt;/p&gt;

&lt;p&gt;There was no structured lead capture. Customer details — names, email addresses, phone numbers, travel dates — were scattered across WhatsApp threads, email chains, and handwritten notes. Following up on an enquiry from two days ago meant searching through message history.&lt;/p&gt;

&lt;p&gt;There was zero compliance infrastructure. Customer ID numbers, credit card details, and personal information flowed through unmonitored channels. South Africa's Protection of Personal Information Act (POPIA) requires that personal data be processed lawfully, with appropriate safeguards. The company was technically exposed on every enquiry that included personal data.&lt;/p&gt;

&lt;p&gt;And there was no visibility into what customers were actually asking. The business had no data on enquiry volume, peak hours, common questions, or conversion rates from enquiry to booking. Decisions about staffing, pricing, and fleet composition were based on gut feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;We deployed seven specialist AI agents as a chat widget on the company's website, powered by &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engagement agent.&lt;/strong&gt; Handles greetings, small talk, and opening conversation. When a visitor says "Hi" or "Good morning," the engagement agent responds warmly and asks how it can help. No RAG retrieval needed — this agent sets the conversational tone and routes deeper questions to specialists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fleet agent.&lt;/strong&gt; Answers vehicle availability and specification queries. "What SUVs do you have?" triggers a RAG search against the company's vehicle catalogue. The agent retrieves relevant fleet information — vehicle types, features, capacity — and presents it conversationally. The knowledge base is updated whenever the fleet changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy agent.&lt;/strong&gt; Handles insurance, waivers, deposits, fuel policy, cross-border rules, and terms and conditions. This agent rewrites the customer's query to bias toward policy-relevant documents before searching the knowledge base. When a customer asks "Can I drive to Namibia?", the agent retrieves the cross-border policy and explains the requirements, additional costs, and required documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quote agent.&lt;/strong&gt; Provides pricing information by searching rate-related documents in the knowledge base. The agent retrieves current pricing and presents it in context. It does not calculate dynamic quotes — it surfaces published rates and directs the customer to complete a booking for a final price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Booking agent.&lt;/strong&gt; Captures booking intent and extracts details. When a customer says "I want to book an SUV from the 15th to the 20th at the airport," the agent extracts the dates, location, and vehicle preference, confirms the details, and directs the customer to complete the reservation through the website or by calling the branch. The agent does not process bookings — it captures the structured intent and ensures the handoff is smooth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Needs assessment agent.&lt;/strong&gt; Handles vague or incomplete queries. When a customer says "I need a car," the agent identifies what information is missing — travel dates, pickup location, vehicle preference — and asks one clarifying question at a time. It does not dump a form. It has a conversation, progressively gathering the details needed to route to the right specialist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation agent.&lt;/strong&gt; Detects frustration, explicit requests for a human, and conversations where the AI is not resolving the issue. This agent does not generate an AI response. It produces a fixed handoff message — "I'll connect you with our team" — and flags the conversation for human follow-up. The detection uses both regex pattern matching (phrases like "speak to a manager," "this is unacceptable") and a frustration classifier that catches excessive capitalisation and repeated negative sentiment.&lt;/p&gt;

&lt;p&gt;All seven agents are orchestrated by a LangGraph state machine that classifies intent on every message and routes to the appropriate specialist. The routing is dynamic — a customer can ask about fleet in one message, switch to pricing in the next, and then ask about insurance, and each message is handled by the right agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The governance layer
&lt;/h2&gt;

&lt;p&gt;Every message through the system — inbound and outbound — is scanned by &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork's governance pipeline&lt;/a&gt; before it reaches the LLM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PII detection runs in real-time.&lt;/strong&gt; South African ID numbers (13-digit format), credit card numbers (Luhn-validated), phone numbers, and email addresses are detected and redacted before the message is processed. The LLM never sees raw PII. The redacted version is what gets stored, what gets sent to the model, and what appears in logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit receipts are generated for every interaction.&lt;/strong&gt; Each governance scan — input and output — produces a receipt with a unique ID, recording what was scanned, what was detected, and what action was taken. These receipts are stored independently of the conversation and can be retrieved by conversation ID, tenant, or time range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;POPIA alignment was built in from day one.&lt;/strong&gt; The client did not need to configure compliance settings, hire a data protection officer for the chatbot, or audit the system after launch. Personal data handling was a design constraint, not an afterthought. When their legal team asked "how does the chatbot handle personal information?", the answer was a one-page technical summary with receipt examples — not a conversation about what needed to be built.&lt;/p&gt;

&lt;p&gt;If governance denies a message — because of a policy violation or detected risk — the system short-circuits. No LLM call, no response generation. The denial is recorded with a receipt, and the customer receives a safe fallback. The system does less work, not more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;These are estimates based on observed usage during the first month of deployment. We qualify them as estimates because the company did not have baseline metrics for pre-deployment comparison in all categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated 70%+ of routine enquiries handled without human intervention.&lt;/strong&gt; Pricing, availability, policy, and general questions are answered by the AI. The remaining 30% includes complex booking modifications, complaints that require human judgement, and edge cases outside the knowledge base. This is consistent with industry benchmarks for domain-specific AI assistants with curated knowledge bases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sub-2-second average response time.&lt;/strong&gt; From message received to first token streamed back to the customer. SSE streaming means the customer sees the response appearing token by token rather than waiting for a complete response. Perceived latency is significantly lower than actual generation time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated lead capture.&lt;/strong&gt; Names, email addresses, phone numbers, and travel dates mentioned in conversations are extracted and structured. Before deployment, this information was scattered across channels. Now it feeds directly into the company's follow-up workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;24/7 availability.&lt;/strong&gt; The most immediate impact. Enquiries that previously waited until the next business day now receive an instant response at any hour. For a tourism-facing business where customers book from different time zones, this is a direct revenue impact — though we do not have the data to quantify it precisely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staff redeployed to higher-value work.&lt;/strong&gt; The three staff members who were previously spending their day answering the same ten questions are now focused on closing bookings, managing VIP accounts, and handling the complex enquiries that the AI escalates. This is not a headcount reduction — it is a reallocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enquiry visibility for the first time.&lt;/strong&gt; The company now has data on what customers ask, when they ask it, which questions lead to bookings, and where the AI struggles. This has informed decisions about fleet composition, pricing, website content, and staffing schedules. Prior to deployment, these were guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python 3.12 + FastAPI&lt;/strong&gt; for the API layer. Async throughout — every external call (LLM, governance, database, cache) is non-blocking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; for multi-agent orchestration. The state machine handles intent classification, agent routing, and response generation as a compiled graph. Adding a new agent means adding a node and an edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic Claude&lt;/strong&gt; for language generation. Claude Haiku for intent classification (speed) and response generation for routine queries. Claude Sonnet available for complex queries requiring deeper reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase with pgvector&lt;/strong&gt; for the knowledge base. Vehicle catalogue, pricing, policies, and FAQs are chunked, embedded, and stored as vectors. RAG retrieval uses cosine similarity with a tuned threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upstash Redis&lt;/strong&gt; for session management. Conversation history is cached with a 24-hour TTL and a rolling window of recent messages for context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSE streaming&lt;/strong&gt; for real-time response delivery. The widget renders tokens as they arrive rather than waiting for the full response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant architecture.&lt;/strong&gt; The same engine serves multiple clients. Each tenant has their own knowledge base, bot configuration, system prompt, and widget styling. Onboarding a new client means configuring a tenant — not deploying new infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with three agents, not seven.&lt;/strong&gt; For an MVP, you need engagement (greetings), a general RAG agent (handles everything with one knowledge base), and escalation (hand-off to humans). The specialist routing — fleet, policy, quote, booking, needs — is a refinement that improves accuracy but is not necessary for a first deployment. We built all seven because we had the architecture ready, but if we were advising a team starting from scratch, we would say: ship three, measure, then specialise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invest more in escalation detection from day one.&lt;/strong&gt; We underestimated how important this would be. The escalation agent is the simplest agent in the system — it returns a fixed message and flags for human follow-up. But detecting when to escalate is the hardest classification problem. Customers express frustration in subtle ways that regex and even LLM classifiers miss. We added the all-caps detector and expanded the pattern list after launch, based on conversations where the AI tried to resolve something a human should have handled. If starting over, escalation detection would be the first thing we tested with real customer data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with real customer data sooner.&lt;/strong&gt; We built the knowledge base from the company's website content, policy documents, and pricing sheets. This covered 80% of what customers ask. The other 20% — questions phrased in ways we did not anticipate, local slang, questions that span multiple categories — only surfaced once real customers started using the system. We refined the knowledge base weekly during the first month. A one-week pilot with live traffic before "launch" would have caught most of these gaps earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the analytics dashboard earlier.&lt;/strong&gt; We added enquiry analytics — question categories, peak hours, escalation rates, unanswered question patterns — after the initial deployment. It should have been in the first release. The client's most common feedback in week one was "this is great, but what are customers actually asking?" The data was in the database. The dashboard to surface it was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this relevant to your business?
&lt;/h2&gt;

&lt;p&gt;This deployment was for vehicle rental, but the pattern applies to any service business that handles a predictable set of customer enquiries: property management, insurance brokers, medical practices, legal intake, hospitality, logistics.&lt;/p&gt;

&lt;p&gt;If your team spends hours each day answering the same questions, if after-hours enquiries go unanswered, if customer data flows through unmonitored channels, and if you have no visibility into what your customers are actually asking — this is solvable.&lt;/p&gt;

&lt;p&gt;The technology exists. The governance layer exists. The deployment timeline is weeks, not months.&lt;/p&gt;

&lt;p&gt;See it in action at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;, or read about the broader thesis behind governed AI agents in &lt;em&gt;The Agent Crisis&lt;/em&gt;, available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by the Tork team. Multi-agent AI with governance for customer-facing deployments. &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>casestudy</category>
      <category>startup</category>
      <category>saas</category>
    </item>
    <item>
      <title>AI Governance Is a Seatbelt, Not Invincibility — And That's the Point</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:46:29 +0000</pubDate>
      <link>https://dev.to/torkjacobs/ai-governance-is-a-seatbelt-not-invincibility-and-thats-the-point-2ikh</link>
      <guid>https://dev.to/torkjacobs/ai-governance-is-a-seatbelt-not-invincibility-and-thats-the-point-2ikh</guid>
      <description>&lt;p&gt;I am going to say something that most governance vendors will not: our product will not prevent every bad thing your AI does.&lt;/p&gt;

&lt;p&gt;It will catch most of them. It will log all of them. And when something slips through — because something will — you will have cryptographic proof of what happened, when, and what your system did about it.&lt;/p&gt;

&lt;p&gt;That is not a failure of governance. That is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invincibility myth
&lt;/h2&gt;

&lt;p&gt;Open any AI governance vendor's website. Count the absolutes. "Complete compliance." "Total protection." "Zero risk deployment." "Bulletproof AI safety."&lt;/p&gt;

&lt;p&gt;These are lies. Polite, well-designed, investor-friendly lies — but lies.&lt;/p&gt;

&lt;p&gt;No PII detection system catches every pattern. South African ID numbers follow a predictable format. Credit card numbers pass the Luhn check. These are detectable. But a customer who writes "my number is nine five zero two zero one five eight zero zero zero eight six" has just bypassed every regex and most ML classifiers. A customer who puts their ID number in an image, or splits it across two messages, or embeds it in a question about someone else — these are edge cases that no scanner handles perfectly.&lt;/p&gt;

&lt;p&gt;No policy enforcement system prevents every hallucination. LLMs are stochastic. They generate plausible text, not verified facts. You can constrain outputs, scan responses, and block known failure patterns — but you cannot guarantee that a model with 175 billion parameters will never produce a sentence you did not anticipate.&lt;/p&gt;

&lt;p&gt;Anyone who tells you otherwise is selling a feeling, not a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seatbelt
&lt;/h2&gt;

&lt;p&gt;A seatbelt does not prevent car crashes. It does not make you a better driver. It does not stop the other driver from running a red light.&lt;/p&gt;

&lt;p&gt;What it does: when the crash happens, it dramatically reduces the damage. It keeps you in the seat instead of through the windshield. It converts a fatal outcome into a survivable one.&lt;/p&gt;

&lt;p&gt;AI governance is a seatbelt.&lt;/p&gt;

&lt;p&gt;It will not prevent every PII leak. It will detect the vast majority of them — in real time, in the request path, before the data reaches the LLM or the database. The ones it misses are logged with enough context to detect them in review.&lt;/p&gt;

&lt;p&gt;It will not prevent every hallucination. It will scan every response against policy rules and flag violations before the customer sees them. The hallucinations it misses are recorded with audit receipts, so when a customer says "your chatbot told me X," you can verify exactly what it said.&lt;/p&gt;

&lt;p&gt;It will not prevent every rogue agent action. It will enforce escalation rules, maintain human override capability, and provide a kill switch that works in seconds, not deployments.&lt;/p&gt;

&lt;p&gt;The seatbelt framing is uncomfortable for sales teams because it admits imperfection. It is comfortable for engineers and compliance officers because it is honest. And in my experience, honest framing builds longer customer relationships than invincibility promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "defensible" actually means
&lt;/h2&gt;

&lt;p&gt;When something goes wrong with your AI — and it will — you end up in one of two positions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position A: "We had no idea."&lt;/strong&gt; No logs. No audit trail. No evidence of what the AI said. No record of governance scans. The regulator, the customer, or the lawyer asks what happened, and you reconstruct it from server logs and Slack threads. Your legal counsel describes this as "indefensible."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position B: "We caught it in 200 milliseconds."&lt;/strong&gt; You have a cryptographic receipt showing exactly what the customer sent, what governance detected, what the AI received (after redaction), what the AI responded, what the output scan flagged, and what action was taken. The receipt has a unique ID, a timestamp, and is stored independently of the conversation. Your legal counsel describes this as "defensible."&lt;/p&gt;

&lt;p&gt;The difference between these two positions is not whether the incident happened. Incidents happen. The difference is whether you can prove you had systems in place, those systems were running, and you responded appropriately.&lt;/p&gt;

&lt;p&gt;This is what governance actually provides: not prevention, but proof.&lt;/p&gt;

&lt;p&gt;In 2023, the Italian data protection authority temporarily banned ChatGPT — not because it made errors, but because OpenAI could not adequately demonstrate how user data was being processed. The issue was not the AI's behaviour. It was the inability to prove what the AI was doing with the data. Governance that generates verifiable receipts addresses this directly.&lt;/p&gt;

&lt;p&gt;When Air Canada's chatbot fabricated a bereavement discount, the company had no audit trail showing what the chatbot was instructed to say versus what it actually said. A governance layer with output scanning would not have prevented the hallucination with certainty. But it would have flagged a response that referenced a policy not present in the knowledge base, and it would have produced a receipt proving the system attempted to catch it.&lt;/p&gt;

&lt;p&gt;Defensible does not mean perfect. It means prepared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers of realistic governance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: Detection
&lt;/h3&gt;

&lt;p&gt;Catch PII. Catch policy violations. Catch anomalies.&lt;/p&gt;

&lt;p&gt;Real-time scanning of every input and every output. Regular expressions for structured data (credit cards, ID numbers, phone numbers). ML classifiers for unstructured PII (names mentioned in context, addresses described in prose). Policy rules for topic boundaries, claim restrictions, and escalation triggers.&lt;/p&gt;

&lt;p&gt;Will you catch 100%? No. Aim for 99%+. The last 1% is why you have Layer 2.&lt;/p&gt;

&lt;p&gt;Detection is not just about blocking bad content. It is about knowing what passed through your system. A scan that returns "allow" is just as important as a scan that returns "deny" — both generate receipts, both are auditable, both prove the system was running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Evidence
&lt;/h3&gt;

&lt;p&gt;Cryptographic receipts for every interaction.&lt;/p&gt;

&lt;p&gt;Every scan — input and output — produces a receipt with a unique ID. The receipt records what was scanned, what was detected, what action was taken, and when. Receipts are stored independently of conversation logs. They cannot be retroactively modified without detection.&lt;/p&gt;

&lt;p&gt;When something goes wrong, you do not grep through log files hoping to find the relevant entry. You query receipts by conversation ID, by tenant, by time range, or by action type. The data is structured, indexed, and verifiable.&lt;/p&gt;

&lt;p&gt;This layer exists for the incidents that Layer 1 misses. Detection failed, but evidence did not. You can prove the system was running, prove what it scanned, and prove the outcome. The gap between "our system should have caught this" and "our system did not catch this, but here is the complete record of what happened" is the gap between a compliance violation and a documented incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Response
&lt;/h3&gt;

&lt;p&gt;When the AI fails, humans take over immediately.&lt;/p&gt;

&lt;p&gt;Escalation rules that trigger on frustration signals, explicit handoff requests, and repeated failed interactions. A kill switch that disables AI responses in seconds — per tenant, per topic, or globally. Human override capability that lets a supervisor intervene in a live conversation.&lt;/p&gt;

&lt;p&gt;Response is the layer most governance vendors ignore because it requires operational design, not just software. A kill switch is useless if nobody is monitoring. Escalation rules are useless if there is no human to escalate to. This layer is as much about process as it is about code.&lt;/p&gt;

&lt;p&gt;The three layers work together. Detection catches most problems before they reach the customer. Evidence ensures every interaction is recorded regardless of what detection caught. Response ensures that when both detection and evidence reveal a problem, a human can act on it immediately.&lt;/p&gt;

&lt;p&gt;No single layer is sufficient. All three together give you a defensible position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this framing sells better
&lt;/h2&gt;

&lt;p&gt;I know this reads like an argument against selling governance. It is the opposite.&lt;/p&gt;

&lt;p&gt;Customers who have been burned by vendor promises are the most skeptical buyers in enterprise software. They have heard "100% uptime," "zero data loss," and "complete protection" before. They know what those claims are worth when the incident report lands on their desk.&lt;/p&gt;

&lt;p&gt;"We will reduce your risk by 95%+ and give you a complete audit trail for the rest" is a statement that a CTO can repeat to their board without feeling like they are being dishonest. "We eliminate all AI risk" is a statement that makes a CTO wonder what you are hiding.&lt;/p&gt;

&lt;p&gt;The seatbelt framing also manages expectations after the sale. Customers who understand that governance is risk reduction — not risk elimination — have fewer complaints when an edge case gets through. They expected it. They planned for it. The audit receipt is there. The escalation path worked. The system performed as described.&lt;/p&gt;

&lt;p&gt;Customers who were sold invincibility experience every edge case as a broken promise. They call support angry. They question the product. They churn.&lt;/p&gt;

&lt;p&gt;Honest framing produces longer contracts. In a market full of absolute claims, "we are really good at this but not perfect, and here is how we handle the imperfection" is a differentiator.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we built this into Tork
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork&lt;/a&gt;, every design decision starts from the assumption that the system will encounter inputs it cannot perfectly handle.&lt;/p&gt;

&lt;p&gt;Every marketing claim is probabilistic, not absolute. We detect PII. We do not claim to detect all PII. We scan outputs. We do not claim to prevent all hallucinations. We generate audit receipts. We do claim those receipts are cryptographic and tamper-evident — because that is a property of the system, not a probabilistic outcome.&lt;/p&gt;

&lt;p&gt;Every governance receipt in &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; is a structured record with a unique ID, stored independently of the conversation. When our customers face a compliance query, they pull receipts — not log files.&lt;/p&gt;

&lt;p&gt;Every escalation path ends at a human. The escalation agent in our multi-agent system does not generate an AI response. It produces a fixed handoff message and routes to a person. No LLM in the loop when a customer is frustrated. This is not a limitation of our AI. It is a design choice that acknowledges the AI's limitations.&lt;/p&gt;

&lt;p&gt;Every tenant has a kill switch. One API call disables AI responses for that tenant. The widget falls back to a contact form. This exists not because we expect to use it often, but because the alternative — a deployment pipeline — takes minutes when you need seconds.&lt;/p&gt;

&lt;p&gt;We built governance as a seatbelt because that is what our customers actually need: a system that reduces risk, proves what happened, and lets humans take over when the AI is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;If your governance vendor promises 100% protection, one of two things is true:&lt;/p&gt;

&lt;p&gt;They do not understand the problem space. PII detection, hallucination prevention, and policy enforcement are probabilistic problems operating on stochastic systems. Anyone who has built these systems knows that edge cases are infinite and perfection is asymptotic.&lt;/p&gt;

&lt;p&gt;Or they understand it perfectly and chose to lie anyway, because "95%+ detection with complete audit trails" does not look as good on a slide deck as "total protection."&lt;/p&gt;

&lt;p&gt;Either way, you should ask harder questions. Ask for false negative rates. Ask what happens when detection fails. Ask to see an audit receipt from a real incident. Ask how fast the kill switch works. Ask who gets paged at 3am when the AI starts misbehaving.&lt;/p&gt;

&lt;p&gt;The vendors who answer these questions clearly are the ones who have thought about failure. The ones who deflect back to "complete protection" have not.&lt;/p&gt;

&lt;p&gt;Governance is risk reduction, not risk elimination. The companies that understand this will build more resilient systems, maintain longer customer relationships, and have better outcomes when — not if — something goes wrong.&lt;/p&gt;

&lt;p&gt;Build your seatbelt. Skip the invincibility cape.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We built &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork&lt;/a&gt; on this philosophy. Governance-first AI for customer-facing deployments. The honest kind. Read more about governed AI agents in The Agent Crisis, available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>governance</category>
      <category>startup</category>
      <category>opinion</category>
    </item>
    <item>
      <title>Building a Multi-Agent Customer Service System with LangGraph — A Practical Guide</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:45:55 +0000</pubDate>
      <link>https://dev.to/torkjacobs/building-a-multi-agent-customer-service-system-with-langgraph-a-practical-guide-43b5</link>
      <guid>https://dev.to/torkjacobs/building-a-multi-agent-customer-service-system-with-langgraph-a-practical-guide-43b5</guid>
      <description>&lt;p&gt;This is not a toy example. We are going to build a multi-agent customer service system where different AI agents handle different types of enquiries — greetings, product queries, pricing, bookings, policy questions, and escalation to humans. By the end, you will have a working LangGraph state machine that classifies intent, routes to the right agent, retrieves relevant context via RAG, and generates a response.&lt;/p&gt;

&lt;p&gt;The code here is drawn from &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt;, a production multi-agent assistant deployed in the vehicle rental industry. I have simplified some of the production concerns (governance, multi-tenancy, observability) to focus on the multi-agent pattern itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why multi-agent over single-prompt
&lt;/h2&gt;

&lt;p&gt;A single prompt can answer a single question well. It falls apart when a customer does this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What SUVs do you have?" → fleet query&lt;br&gt;
"How much for 3 days?" → pricing&lt;br&gt;
"OK book it for next Friday" → booking intent&lt;br&gt;
"What's your fuel policy?" → policy lookup&lt;br&gt;
"Actually this is too expensive, let me speak to someone" → escalation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A single system prompt that tries to handle fleet knowledge, pricing logic, booking flows, policy details, and escalation detection simultaneously is a prompt that does none of them well. It gets long, contradictory, and fragile. Change one instruction and something else breaks.&lt;/p&gt;

&lt;p&gt;The multi-agent approach separates concerns. Each agent has a focused system prompt and searches a focused part of the knowledge base. The routing layer decides which agent handles each message. The agents do not need to know about each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up LangGraph
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;langgraph anthropic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangGraph gives you three primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;StateGraph&lt;/strong&gt;: A directed graph where state flows from node to node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nodes&lt;/strong&gt;: Async functions that receive state and return updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edges&lt;/strong&gt;: Connections between nodes — either fixed or conditional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model: state enters the graph, flows through nodes that transform it, and exits with a result. Each node reads what it needs from state and returns only the fields it wants to update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the state
&lt;/h2&gt;

&lt;p&gt;The state is a &lt;code&gt;TypedDict&lt;/code&gt; that carries everything the graph needs. Every node reads from it and writes to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Input — set once at the start
&lt;/span&gt;    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

    &lt;span class="c1"&gt;# Tenant context — set by resolve_tenant
&lt;/span&gt;    &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Routing — set by classify_intent
&lt;/span&gt;    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greeting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complaint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;current_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# RAG context — set by specialist agents
&lt;/span&gt;    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;

    &lt;span class="c1"&gt;# Response — set by generate_response
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;escalated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;

    &lt;span class="c1"&gt;# Conversation history
&lt;/span&gt;    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field has a clear owner — the node that sets it. This matters because LangGraph merges node return values into the state. If two nodes both return &lt;code&gt;chunks&lt;/code&gt;, the last one wins. By designing the state so each field has one writer, you avoid subtle bugs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;intent&lt;/code&gt; field uses a &lt;code&gt;Literal&lt;/code&gt; type. This is documentation, not enforcement — Python will not reject an invalid intent at runtime. But it makes the valid values explicit for anyone reading the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent classification
&lt;/h2&gt;

&lt;p&gt;The classifier is the routing brain. It takes the user's message and returns one intent label. We use Claude Haiku because this is a low-stakes, high-frequency call — it needs to be fast, not deep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify_intent_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Pre-check: skip the LLM for obvious escalations
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;matches_escalation_patterns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify the user message into exactly one intent: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greeting, fleet_query, pricing, booking, policy, complaint, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general, escalate. Respond with ONLY the intent word, nothing else.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; when the user wants to speak to a human, manager, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;or supervisor, or expresses strong frustration or anger.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;valid_intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greeting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complaint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;valid_intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design decisions here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;temperature=0&lt;/code&gt;&lt;/strong&gt; — We want deterministic classification. The same message should always route to the same agent. Temperature zero does not guarantee this (Claude is not fully deterministic), but it gets close enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;max_tokens=10&lt;/code&gt;&lt;/strong&gt; — The response should be a single word. Setting a low token limit prevents the model from writing an explanation. If it tries to say "I think this is a fleet_query because..." it gets cut off after the intent word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pre-check pattern&lt;/strong&gt; — Before calling the LLM, we check for obvious escalation signals with regex. This catches "speak to a manager," "this is unacceptable," and all-caps messages without burning an API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;ESCALATION_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bspeak to (a )?(human|person|agent|manager|supervisor)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bmanager\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bhuman agent\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bescalate\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bcomplaint\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bunacceptable\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matches_escalation_patterns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ESCALATION_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="c1"&gt;# Frustration indicator: excessive caps
&lt;/span&gt;    &lt;span class="n"&gt;alpha_chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isalpha&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha_chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;upper_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;alpha_chars&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isupper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha_chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;upper_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The all-caps check is important. Customers who type "THIS IS RIDICULOUS I HAVE BEEN WAITING FOR AN HOUR" are not asking a question. They want a human. The LLM might classify this as "complaint" and try to generate a soothing response. The regex pre-check catches it and routes directly to escalation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specialist agent nodes
&lt;/h2&gt;

&lt;p&gt;Each agent is an async function that receives the graph state and returns updates. The pattern is consistent: read the message, query the relevant knowledge, and return chunks for the response generator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fleet search&lt;/strong&gt; — queries the full knowledge base for product information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fleet_search_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Policy search&lt;/strong&gt; — rewrites the query to bias toward policy-related chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;policy_search_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;policy_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy terms conditions: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Engagement&lt;/strong&gt; — handles greetings. No RAG needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;engagement_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Needs assessment&lt;/strong&gt; — the interesting one. When the user's query is too vague to route to a specialist, this agent checks what information is missing and asks a clarifying question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.models.schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChunkResult&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assess_missing_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;all_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;all_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_text&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\d{1,2}[/-]\d{1,2}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;all_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;travel dates (pickup and return)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cape town&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;johannesburg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preferred pickup location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sedan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bakkie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;van&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type of vehicle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;needs_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;assess_missing_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;focus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s query is missing some details. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Still needed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Politely ask about: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keep it conversational — don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t list all missing items at once.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer has provided enough context. Answer helpfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;context_chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChunkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs_assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context_chunk&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The needs agent does not call RAG. It manufactures a synthetic chunk that instructs the response generator on what to ask. This is a useful pattern: you can steer the final LLM response by injecting context as if it came from RAG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation&lt;/strong&gt; — the agent that does not generate an AI response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;escalation_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll connect you with our team. A human agent will reach out shortly.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The escalation agent returns a fixed response and skips the LLM entirely. This is deliberate. When a customer is frustrated enough to ask for a human, the worst thing you can do is run their message through another round of AI. The escalation node sets &lt;code&gt;response&lt;/code&gt; directly and gets routed past &lt;code&gt;generate_response&lt;/code&gt; to &lt;code&gt;save_message&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The routing edge
&lt;/h2&gt;

&lt;p&gt;The routing function maps intents to agent node names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_by_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;routing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greeting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complaint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions to note:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complaints route to policy search.&lt;/strong&gt; A complaint like "your insurance policy is unfair" is best addressed by surfacing the actual policy. The policy agent retrieves the relevant terms, and the response generator can explain them. Routing complaints to a generic agent produces vague apologies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown intents default to engagement.&lt;/strong&gt; If the classifier returns something unexpected, we fall back to the friendliest agent rather than the most capable. A warm "Hi, how can I help?" is better than a confused attempt at fleet search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response generation
&lt;/h2&gt;

&lt;p&gt;The response generator is the only node that calls the LLM with the full context. It combines everything the specialist agent prepared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_response_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bot_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;our company&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Be friendly, concise, and helpful.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Build conversation from session history
&lt;/span&gt;    &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

    &lt;span class="c1"&gt;# Agent-provided RAG chunks become part of the system prompt
&lt;/span&gt;    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;response_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;llm_router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;llm_router.generate&lt;/code&gt;, the RAG chunks are appended to the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;full_system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;full_system&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Use the following knowledge base excerpts to answer. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If the information is not in the excerpts, say you don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t have &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;that specific information and suggest they contact the business directly.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;model_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5-20250514&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-0-20250514&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;raw_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bot_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;full_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "suggest they contact the business directly" fallback is important. When RAG returns no relevant chunks, the LLM knows it should not hallucinate an answer. This one instruction prevents the most common failure mode in RAG systems: confident fabrication when the knowledge base has a gap.&lt;/p&gt;

&lt;p&gt;For real-time delivery, we stream the response with Server-Sent Events instead of waiting for the full completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StreamingResponse&lt;/span&gt;

&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;event_generator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# ... tenant resolution, governance, RAG ...
&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;conversation_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;event_generator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The widget on the frontend reads these events with &lt;code&gt;EventSource&lt;/code&gt; and renders tokens as they arrive. The perceived latency drops from seconds to milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Here is the complete graph definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_chat_graph&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Add all nodes
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolve_tenant_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;govern_input_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;classify_intent_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engagement_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fleet_search_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy_search_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needs_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;escalation_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate_response_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;govern_output_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_message_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Entry: START → resolve tenant → governance input scan
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# If governance denies the input, skip to save (no LLM call)
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_after_govern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Route to specialist agent based on classified intent
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_by_intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# All agents (except escalation) → generate response
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Escalation skips LLM — fixed response, straight to save
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Response → output governance → save → done
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;chat_graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_chat_graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chat_graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainvoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What SUVs do you have available?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-rentals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess_abc123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;# "fleet_query"
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;# "fleet"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph handles the full journey: resolve the tenant, scan the input, classify "What SUVs do you have available?" as &lt;code&gt;fleet_query&lt;/code&gt;, route to the fleet agent, retrieve relevant vehicle chunks from the knowledge base, generate a response with Claude, scan the output, and save the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing from this tutorial
&lt;/h2&gt;

&lt;p&gt;This guide covers the multi-agent routing pattern. A production deployment needs several more layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance.&lt;/strong&gt; Every message — inbound and outbound — should pass through a compliance layer that detects PII, enforces policies, and generates audit receipts. In the graph above, &lt;code&gt;govern_input&lt;/code&gt; and &lt;code&gt;govern_output&lt;/code&gt; are placeholders. In production, these call &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork's governance pipeline&lt;/a&gt; to scan every interaction before it reaches the LLM and before the response reaches the customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session management.&lt;/strong&gt; The &lt;code&gt;history&lt;/code&gt; field in the state needs to be populated from a persistent session store. We use Upstash Redis with a 24-hour TTL and a 10-message rolling window. Without this, every message is context-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; Without it, a single user can exhaust your API budget in minutes. Rate limit per session, per tenant, and globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy.&lt;/strong&gt; The &lt;code&gt;tenant&lt;/code&gt; and &lt;code&gt;bot_config&lt;/code&gt; fields hint at this. In production, each tenant gets their own system prompt, model selection, knowledge base, and widget configuration. The same graph serves every tenant — the state carries the customisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability.&lt;/strong&gt; Add tracing to every node. You need to know how long intent classification takes, which agent was selected, how many RAG chunks were retrieved, and what the LLM's token usage was. LangSmith integrates well with LangGraph for this.&lt;/p&gt;

&lt;p&gt;If you want to see all of these concerns implemented together, &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; is the production version of what this tutorial describes. We also wrote about the broader case for governed AI agents in &lt;em&gt;The Agent Crisis&lt;/em&gt;, available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or building something similar? Reach out at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langgraph</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your AI Chatbot Needs Governance Before It Needs Features</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:45:22 +0000</pubDate>
      <link>https://dev.to/torkjacobs/why-your-ai-chatbot-needs-governance-before-it-needs-features-233c</link>
      <guid>https://dev.to/torkjacobs/why-your-ai-chatbot-needs-governance-before-it-needs-features-233c</guid>
      <description>&lt;p&gt;You are about to deploy an AI chatbot. Your backlog has 40 items on it: better prompts, streaming responses, multi-language support, analytics dashboard, Slack integration. Governance is item 38, somewhere between "dark mode" and "maybe add a mascot."&lt;/p&gt;

&lt;p&gt;This is the wrong order. And the consequences are not theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature trap
&lt;/h2&gt;

&lt;p&gt;In March 2023, Samsung engineers pasted proprietary source code into ChatGPT on three separate occasions. The company banned the tool within weeks, but the data was already in OpenAI's training pipeline. In late 2023, Air Canada's chatbot invented a bereavement discount policy that did not exist — and a tribunal ruled the airline had to honour it. In 2024, a New York law firm submitted a court filing with six fabricated case citations generated by ChatGPT. The lawyers were sanctioned.&lt;/p&gt;

&lt;p&gt;These are not edge cases from careless teams. These are what happens when capable AI is deployed without governance. The chatbot works. It sounds confident. It handles 80% of queries well. And then it stores a customer's ID number in plaintext, invents a policy, or leaks internal data — and the feature backlog becomes irrelevant because you are now managing a crisis.&lt;/p&gt;

&lt;p&gt;Every team that has shipped an ungoverned AI chatbot believed they would "add governance later." Later does not arrive until after the incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI governance actually means
&lt;/h2&gt;

&lt;p&gt;Governance is not a committee. It is not a PDF that legal signs off on. It is a middleware layer — running code that sits between the user and the LLM, inspecting every message in both directions.&lt;/p&gt;

&lt;p&gt;Every input is scanned before the model sees it. Every output is checked before the customer sees it. Every interaction generates a cryptographic receipt that proves what happened.&lt;/p&gt;

&lt;p&gt;Think of governance as a seatbelt, not a speed limit. A speed limit slows you down. A seatbelt lets you drive at full speed with a plan for when things go wrong. Governed AI is not slower AI. It is AI that you can defend, audit, and trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 non-negotiable capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. PII detection
&lt;/h3&gt;

&lt;p&gt;A customer types: "My ID number is 9502015800086 and my card is 4532-XXXX-XXXX-1234."&lt;/p&gt;

&lt;p&gt;An ungoverned chatbot stores this in the conversation log, sends it to the LLM provider's API, and maybe writes it to an analytics database. That is three copies of sensitive data created in under a second.&lt;/p&gt;

&lt;p&gt;A governed chatbot detects the PII before it leaves your infrastructure. The ID number is redacted. The card number is masked. The LLM receives the sanitised version. The original is never stored.&lt;/p&gt;

&lt;p&gt;This is not optional. South Africa's POPIA Act requires that personal information be processed only for the purpose it was collected. Europe's GDPR requires data minimisation — you cannot store what you do not need. California's CCPA gives consumers the right to know what data you have collected. If your AI chatbot is hoovering up PII without detection, you are violating all of them simultaneously.&lt;/p&gt;

&lt;p&gt;Real-time PII detection is the baseline. Everything else is built on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Audit receipts
&lt;/h3&gt;

&lt;p&gt;When a regulator, a customer, or a lawyer asks "what did your AI say to this person on Tuesday at 14:32?", you need an answer. Not a log file. Not "we think it said something like this." A tamper-proof receipt.&lt;/p&gt;

&lt;p&gt;An audit receipt is a record of every interaction: what the user sent, what the AI received (after redaction), what the AI responded, and what governance actions were taken. Each receipt has a unique ID, a timestamp, and is stored independently of the conversation log.&lt;/p&gt;

&lt;p&gt;This is the difference between "we have logs" and "we have evidence." Logs can be edited. Receipts are cryptographically verifiable.&lt;/p&gt;

&lt;p&gt;The Air Canada case would have gone differently if the company could have produced an audit trail showing that the chatbot's bereavement policy response was a hallucination that governance should have caught. Instead, they had no trail at all, and the tribunal treated the chatbot's statement as if it were company policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Policy enforcement
&lt;/h3&gt;

&lt;p&gt;Your AI should have boundaries that are defined in code, not in hope.&lt;/p&gt;

&lt;p&gt;Policy enforcement means defining what your AI can and cannot do: which topics it can discuss, which claims it can make, when it must escalate to a human, and what happens when it does not know the answer. These rules are evaluated on every message, not as a system prompt suggestion that the model might ignore.&lt;/p&gt;

&lt;p&gt;A system prompt that says "do not discuss competitors" is a request. A governance policy that scans the output and blocks competitor mentions is enforcement. The difference matters when your chatbot decides to be helpful and recommends a rival's product.&lt;/p&gt;

&lt;p&gt;Escalation triggers are part of policy enforcement. When a customer says "I want to speak to a manager" or "this is completely unacceptable," the governed response is not another AI-generated apology. It is a structured handoff to a human, with the conversation context attached. The ungoverned response is three more paragraphs of synthetic empathy that makes the customer angrier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of retrofitting
&lt;/h2&gt;

&lt;p&gt;Adding governance after launch is not twice as hard. It is an order of magnitude harder.&lt;/p&gt;

&lt;p&gt;Your database already has six months of unscanned conversations. Some contain PII. You do not know which ones. A full scan of historical data is a project in itself — and it reveals problems you now have to report under data protection law.&lt;/p&gt;

&lt;p&gt;Your audit trail has gaps. For every conversation that happened before governance was added, you cannot prove what the AI said. If a customer dispute arises from that period, your legal position is "we don't know."&lt;/p&gt;

&lt;p&gt;Your architecture was not designed for middleware. Adding an inspection layer between the user and the LLM means reworking your request pipeline, your streaming implementation, your error handling, and your tests. Features that were built assuming direct LLM access now need to account for governance latency, redaction, and denial responses.&lt;/p&gt;

&lt;p&gt;And your customers have already formed trust expectations. If your chatbot has been freely accepting ID numbers for six months and suddenly starts redacting them, customers notice. The transition itself becomes a support issue.&lt;/p&gt;

&lt;p&gt;Build governance into the foundation. Then build features on top of a system you can trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we did it at Tork
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork&lt;/a&gt;, governance is not a wrapper around the AI. It is a node in the state machine.&lt;/p&gt;

&lt;p&gt;Every message follows the same path: tenant resolution, then governance input scan, then intent classification, then agent routing, then response generation, then governance output scan, then storage. The LLM never sees raw PII. The response never reaches the customer without an output scan. Every step produces a receipt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User message
  → Tenant resolution
  → Governance input scan (PII detect, policy check, receipt generated)
  → Intent classification
  → Specialist agent (fleet, policy, booking, etc.)
  → Response generation
  → Governance output scan (data leak check, policy check, receipt generated)
  → Customer receives response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If governance denies the input, the graph short-circuits. No LLM call, no agent routing, no response generation. The denial is recorded with a receipt, and the customer gets a safe fallback message. The system does less work, not more.&lt;/p&gt;

&lt;p&gt;If governance is temporarily unreachable, the system degrades gracefully — messages are allowed through with a logged warning, and the governance scan is retried asynchronously. No single point of failure blocks the customer experience.&lt;/p&gt;

&lt;p&gt;This architecture means governance cannot be bypassed by a new feature, a new endpoint, or a developer who forgets to add the middleware. It is structural, not procedural.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical checklist
&lt;/h2&gt;

&lt;p&gt;Before you deploy any AI system that talks to customers, answer these seven questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it detect PII in real-time?&lt;/strong&gt;&lt;br&gt;
Not in a batch job overnight. In the request path, before the data is stored or sent to a third-party API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it generate audit receipts?&lt;/strong&gt;&lt;br&gt;
Not log lines. Structured, immutable records with unique IDs that can be retrieved by conversation, by user, or by time range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you prove what it said to a regulator?&lt;/strong&gt;&lt;br&gt;
If a data protection authority asks for the full interaction history for a specific customer, can you produce it within 72 hours? GDPR requires this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it have escalation rules?&lt;/strong&gt;&lt;br&gt;
When the AI is out of its depth or the customer is frustrated, does it hand off to a human? Or does it keep generating responses and hoping for the best?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a human override?&lt;/strong&gt;&lt;br&gt;
Can a supervisor intervene in a live conversation? Can you disable the AI for a specific tenant, a specific topic, or globally — without a deployment?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it compliant with local data protection law?&lt;/strong&gt;&lt;br&gt;
POPIA in South Africa. GDPR in Europe. CCPA in California. LGPD in Brazil. The law your AI needs to comply with depends on where your customers are, not where your servers are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you turn it off in 5 seconds?&lt;/strong&gt;&lt;br&gt;
Not "start a deployment." Not "merge a PR." A kill switch. If the AI starts generating harmful content at scale, how fast can you stop it?&lt;/p&gt;

&lt;p&gt;If you answered "no" to more than two of these, you are not ready to deploy. Fix governance first. The features can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start governed
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; because we needed a customer assistant that we could defend to a regulator, explain to a compliance officer, and trust with real customer data. The governance layer is not a premium add-on. It is the first thing that runs.&lt;/p&gt;

&lt;p&gt;If you are building AI for customer-facing use cases, start with governance. Not because it is the responsible thing to do — though it is — but because retrofitting it later will cost you more in engineering time, legal exposure, and customer trust than building it right from the start.&lt;/p&gt;

&lt;p&gt;Try Tork's governance-first approach at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;. Free to start.&lt;/p&gt;

&lt;p&gt;If you want the deeper thesis on why governed AI agents are the next frontier — and why most current deployments are dangerously ungovened — we wrote a book about it. &lt;em&gt;The Agent Crisis&lt;/em&gt; is available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by the Tork team. Governance-first AI for customer-facing deployments. &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>governance</category>
      <category>compliance</category>
      <category>security</category>
    </item>
    <item>
      <title>We Built a Multi-Agent AI Customer Assistant with Built-In Governance — Here's How</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:44:50 +0000</pubDate>
      <link>https://dev.to/torkjacobs/we-built-a-multi-agent-ai-customer-assistant-with-built-in-governance-heres-how-128p</link>
      <guid>https://dev.to/torkjacobs/we-built-a-multi-agent-ai-customer-assistant-with-built-in-governance-heres-how-128p</guid>
      <description>&lt;p&gt;Most AI chatbots are a single model behind an API. One prompt, one response, no guardrails. That works for demos. It does not work when a customer asks about fleet availability, gets quoted a price, then wants to book — all in the same conversation. And it definitely does not work when that customer shares their ID number and you have no PII detection, no audit trail, and no compliance story.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;Tork Chat&lt;/a&gt; to solve this. It is a multi-agent AI customer assistant with governance built into every message, not bolted on after. This post walks through how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture at a glance
&lt;/h2&gt;

&lt;p&gt;The stack: Python 3.12, FastAPI, LangGraph, Anthropic Claude (Haiku for speed, Sonnet for depth), Supabase with pgvector for RAG, Upstash Redis for sessions, and Server-Sent Events for real-time streaming.&lt;/p&gt;

&lt;p&gt;Every customer message passes through a state machine — not a single prompt chain — where specialist agents handle different parts of the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 agents, one graph
&lt;/h2&gt;

&lt;p&gt;We use LangGraph's &lt;code&gt;StateGraph&lt;/code&gt; to orchestrate seven specialist agents. Each agent handles a specific customer intent: engagement (greetings, chitchat), fleet search (vehicle availability), policy lookup (insurance, deposits, fuel policy), quoting (pricing calculations), booking (reservation flow), needs assessment (open-ended questions), and escalation (hand-off to humans).&lt;/p&gt;

&lt;p&gt;Here is the core graph definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_chat_graph&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Add nodes
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolve_tenant_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;govern_input_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;classify_intent_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engagement_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fleet_search_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy_search_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needs_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;escalation_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate_response_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;govern_output_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_message_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Entry: resolve tenant, then governance scan
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolve_tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# If governance denies, skip straight to save
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_after_govern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Route to specialist agent by intent
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_intent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_by_intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Agents feed into response generation
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fleet_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Escalation skips LLM — goes directly to save
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Response → output governance → save → done
&lt;/span&gt;    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;govern_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state that flows through this graph is a typed dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# classified by Haiku
&lt;/span&gt;    &lt;span class="n"&gt;current_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;                &lt;span class="c1"&gt;# RAG results
&lt;/span&gt;    &lt;span class="n"&gt;input_receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;output_receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;escalated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Intent classification uses Claude Haiku with &lt;code&gt;temperature=0&lt;/code&gt; and a constrained prompt that returns a single word. A regex pre-check catches obvious escalation patterns (requests for a human, excessive caps, frustration phrases) before the LLM is even called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance is not optional
&lt;/h2&gt;

&lt;p&gt;Every message — inbound and outbound — passes through &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork's governance pipeline&lt;/a&gt; before it reaches the LLM. This is not content moderation. It is structured compliance: PII detection with automatic redaction, policy violation scanning, and cryptographic audit receipts for every interaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TorkGovernance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tork-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer-assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;govern_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GovernanceResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;# allow, redact, or deny
&lt;/span&gt;            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redacted_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receipt_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;pii_detected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pii_detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three possible outcomes: &lt;strong&gt;allow&lt;/strong&gt; (pass through), &lt;strong&gt;redact&lt;/strong&gt; (PII stripped, original never reaches the LLM), or &lt;strong&gt;deny&lt;/strong&gt; (message blocked entirely). Every scan produces a receipt ID that is stored alongside the conversation in the database. If a regulator asks "what did the AI see and what did it respond?", the answer is a database query away.&lt;/p&gt;

&lt;p&gt;The governance node sits at position two in the graph — right after tenant resolution and before anything else. If governance denies the input, the graph short-circuits to &lt;code&gt;save_message&lt;/code&gt; without ever calling the LLM. The denial is still recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSE streaming for real-time responses
&lt;/h2&gt;

&lt;p&gt;Nobody wants to stare at a spinner for three seconds. We use Server-Sent Events to stream tokens as they are generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;event_generator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_sse_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;# ... tenant resolution, governance, RAG ...
&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;llm_router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_sse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_sse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_receipt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_receipt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...}})&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_sse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;event_generator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event stream carries five event types: &lt;code&gt;token&lt;/code&gt; (each text chunk), &lt;code&gt;sources&lt;/code&gt; (RAG retrieval results), &lt;code&gt;governance&lt;/code&gt; (the input and output receipt metadata), &lt;code&gt;typing&lt;/code&gt; (UI indicators), and &lt;code&gt;done&lt;/code&gt; (the conversation ID for persistence). The widget on the frontend reads these events and renders tokens as they arrive. The governance metadata arrives after the full response, so the widget can display a "governed" badge without blocking the stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenant by design
&lt;/h2&gt;

&lt;p&gt;Tork Chat is multi-tenant from the ground up. Each tenant gets their own bot configuration (system prompt, model selection, temperature), knowledge base (RAG scoped by tenant ID in Supabase), and widget styling. Tenant configs are cached in Redis with a 5-minute TTL.&lt;/p&gt;

&lt;p&gt;This means the same engine that powers a vehicle rental assistant can also power a property management chatbot or a legal intake bot — each with their own personality, knowledge, and governance rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Intent classification accuracy matters more than response quality.&lt;/strong&gt; A wrong classification routes the customer to the wrong agent, which retrieves the wrong context, which generates a plausible but incorrect answer. We found that Claude Haiku at temperature zero with a tightly constrained system prompt ("respond with ONLY the intent word") achieves reliable classification. Adding a regex pre-check for escalation patterns caught edge cases the LLM missed — particularly frustrated customers using all-caps or demanding a human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation detection saves your reputation.&lt;/strong&gt; The escalation agent does not generate a response. It produces a structured handoff message with the conversation summary and immediately saves. No LLM in the loop for angry customers. This was a deliberate design choice after observing that LLMs tend to be overly apologetic when they should be connecting the customer to a real person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance is a feature, not a burden.&lt;/strong&gt; Every tenant we have spoken to asks about compliance within the first three questions. PII detection and audit trails are not "nice to have" — they are table stakes for deploying AI in customer-facing roles. Building governance into the graph (rather than wrapping it around the API) means it cannot be bypassed. It is a node in the state machine, not middleware that can be skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation is non-negotiable.&lt;/strong&gt; If governance is unreachable, the message is allowed through with a logged warning. If Redis is down, sessions fall back to in-memory. If RAG returns no chunks, the LLM is instructed to say it does not have the information and suggest contacting the business directly. Every external dependency has a fallback path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world deployment
&lt;/h2&gt;

&lt;p&gt;Tork Chat is currently deployed in the vehicle rental industry, handling fleet availability queries, pricing questions, booking flows, insurance and deposit policies, and after-hours support. The system runs 24/7 and escalates to human agents when it detects frustration or explicit handoff requests.&lt;/p&gt;

&lt;p&gt;The widget is embeddable on any website via a script tag, and new tenants can onboard at &lt;a href="https://chat.tork.network/onboard" rel="noopener noreferrer"&gt;chat.tork.network/onboard&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You can see Tork Chat in action at &lt;a href="https://tork.network/chat" rel="noopener noreferrer"&gt;tork.network/chat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are interested in the broader thesis behind governed AI agents — why compliance-first design is the next frontier for AI deployment — we wrote a book about it. &lt;em&gt;The Agent Crisis&lt;/em&gt; is available free at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by the Tork team. Questions, feedback, or want to deploy Tork Chat for your business? Reach out at &lt;a href="https://tork.network" rel="noopener noreferrer"&gt;tork.network&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>governance</category>
      <category>langgraph</category>
    </item>
    <item>
      <title>Add AI Governance to Your Agent in 5 Minutes with Tork SDK</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Wed, 04 Mar 2026 07:43:27 +0000</pubDate>
      <link>https://dev.to/torkjacobs/add-ai-governance-to-your-agent-in-5-minutes-with-tork-sdk-32m2</link>
      <guid>https://dev.to/torkjacobs/add-ai-governance-to-your-agent-in-5-minutes-with-tork-sdk-32m2</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Add&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Governance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Agent&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Minutes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tork&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SDK"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Learn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;implement&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;comprehensive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;including&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PII&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;detection,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;enforcement,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;compliance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;monitoring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tork&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SDK&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;just&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;governance&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Add AI Governance to Your Agent in 5 Minutes with Tork SDK&lt;/span&gt;

AI agents are transforming business operations, but with great power comes great responsibility. As AI agents handle increasingly sensitive data and make autonomous decisions, implementing proper governance isn't just a nice-to-have—it's essential for compliance, security, and ethical AI deployment.

The challenge? Traditional governance solutions are complex, time-consuming to implement, and often require extensive infrastructure changes. What if you could add comprehensive AI governance to your existing agent in just 5 minutes?

&lt;span class="gu"&gt;## Why AI Agents Need Governance&lt;/span&gt;

Modern AI agents process vast amounts of data, interact with users, and make decisions that can impact businesses and individuals. Without proper governance:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Sensitive data exposure**&lt;/span&gt;: Agents may inadvertently process or leak PII, financial data, or healthcare information
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Compliance violations**&lt;/span&gt;: Failure to meet GDPR, HIPAA, SOC 2, or other regulatory requirements
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Audit trail gaps**&lt;/span&gt;: Lack of visibility into agent decisions and data handling
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Policy drift**&lt;/span&gt;: No mechanism to enforce organizational AI policies consistently

The solution? Implementing governance that monitors, controls, and audits your AI agent's behavior in real-time.

&lt;span class="gu"&gt;## Enter Tork Network: Governance Made Simple&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Tork Network&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://tork.network&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; provides an AI governance platform that detects PII in under 1ms, enforces policies across 79+ compliance frameworks, and provides cryptographic compliance receipts—all through simple SDK integration.

Let's walk through implementing comprehensive governance in your AI agent.

&lt;span class="gu"&gt;## Step 1: Install the Tork SDK (30 seconds)&lt;/span&gt;

Choose your preferred language and install the SDK:

&lt;span class="gu"&gt;### Python&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
pip install tork&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### JavaScript/TypeScript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
npm install @tork/sdk&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### Go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
go get github.com/tork-network/tork-go&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Step 2: Initialize Your Governance Client (1 minute)

Set up your Tork client with your API key:

### Python Example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;br&gt;
from tork import TorkClient&lt;br&gt;
import os&lt;/p&gt;
&lt;h1&gt;
  
  
  Initialize client
&lt;/h1&gt;

&lt;p&gt;client = TorkClient(&lt;br&gt;
    api_key=os.getenv("TORK_API_KEY"),&lt;br&gt;
    environment="production"  # or "sandbox" for testing&lt;br&gt;
)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### JavaScript Example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;br&gt;
import { TorkClient } from '@tork/sdk';&lt;/p&gt;

&lt;p&gt;const client = new TorkClient({&lt;br&gt;
    apiKey: process.env.TORK_API_KEY,&lt;br&gt;
    environment: 'production'&lt;br&gt;
});&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Step 3: Add Pre-Processing Governance (2 minutes)

Implement governance checks before your agent processes user input:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;br&gt;
async def process_user_input(user_message: str, user_id: str):&lt;br&gt;
    # Step 1: Detect and classify sensitive data&lt;br&gt;
    governance_result = await client.analyze_content(&lt;br&gt;
        content=user_message,&lt;br&gt;
        user_id=user_id,&lt;br&gt;
        policies=[&lt;br&gt;
            "pii-detection",&lt;br&gt;
            "gdpr-compliance", &lt;br&gt;
            "data-minimization"&lt;br&gt;
        ]&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Step 2: Check if content violates policies
if governance_result.has_violations():
    # Handle policy violations
    for violation in governance_result.violations:
        print(f"Policy violation: {violation.policy_name}")
        print(f"Detected: {violation.detected_patterns}")

    # Return sanitized content or block request
    if governance_result.risk_level == "HIGH":
        return {"error": "Content blocked due to policy violation"}
    else:
        # Use sanitized version
        user_message = governance_result.sanitized_content

# Step 3: Proceed with agent processing
agent_response = await your_agent_process(user_message)

return agent_response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Step 4: Add Post-Processing Governance (1 minute)

Ensure your agent's responses also comply with governance policies:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;br&gt;
python&lt;br&gt;
async def validate_agent_response(response: str, context: dict):&lt;br&gt;
    # Analyze agent output for compliance&lt;br&gt;
    output_analysis = await client.analyze_output(&lt;br&gt;
        content=response,&lt;br&gt;
        context=context,&lt;br&gt;
        compliance_frameworks=["SOC2", "GDPR", "HIPAA"]&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Generate compliance receipt
compliance_receipt = await client.generate_receipt(
    transaction_id=context.get("transaction_id"),
    analysis_result=output_analysis
)

# Log for audit trail
await client.log_interaction(
    user_id=context.get("user_id"),
    input_analysis=context.get("input_analysis"),
    output_analysis=output_analysis,
    compliance_receipt=compliance_receipt
)

return {
    "response": output_analysis.sanitized_content,
    "compliance_receipt": compliance_receipt.signature,
    "risk_score": output_analysis.risk_score
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Step 5: Complete Integration Example (1 minute)

Here's a complete example showing governance integration with a typical AI agent:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;br&gt;
python&lt;br&gt;
import asyncio&lt;br&gt;
from tork import TorkClient&lt;br&gt;
from datetime import datetime&lt;/p&gt;

&lt;p&gt;class GovernedAIAgent:&lt;br&gt;
    def &lt;strong&gt;init&lt;/strong&gt;(self, tork_api_key: str):&lt;br&gt;
        self.tork = TorkClient(api_key=tork_api_key)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async def handle_request(self, user_message: str, user_id: str):
    transaction_id = f"txn_{datetime.now().isoformat()}"

    try:
        # Pre-processing governance
        input_analysis = await self.tork.analyze_content(
            content=user_message,
            user_id=user_id,
            transaction_id=transaction_id,
            policies=["pii-detection", "content-safety", "gdpr-compliance"]
        )

        # Block high-risk content
        if input_analysis.risk_level == "HIGH":
            return {
                "error": "Request blocked for policy violation",
                "violation_id": input_analysis.violation_id
            }

        # Process with your AI agent
        processed_input = input_analysis.sanitized_content
        agent_response = await self.generate_response(processed_input)

        # Post-processing governance
        output_analysis = await self.tork.analyze_output(
            content=agent_response,
            transaction_id=transaction_id,
            compliance_frameworks=["SOC2", "GDPR"]
        )

        # Generate cryptographic compliance receipt
        receipt = await self.tork.generate_receipt(
            transaction_id=transaction_id,
            input_analysis=input_analysis,
            output_analysis=output_analysis
        )

        return {
            "response": output_analysis.sanitized_content,
            "governance": {
                "risk_score": output_analysis.risk_score,
                "compliance_receipt": receipt.hmac_signature,
                "frameworks_validated": receipt.frameworks
            }
        }

    except Exception as e:
        # Log governance failures
        await self.tork.log_error(
            transaction_id=transaction_id,
            error=str(e),
            user_id=user_id
        )
        raise

async def generate_response(self, message: str):
    # Your existing AI agent logic here
    return f"Agent response to: {message}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Usage
&lt;/h1&gt;

&lt;p&gt;agent = GovernedAIAgent(tork_api_key="your_api_key")&lt;br&gt;
result = await agent.handle_request(&lt;br&gt;
    user_message="Hello, my SSN is 123-45-6789",&lt;br&gt;
    user_id="user_123"&lt;br&gt;
)&lt;br&gt;
print(result)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Real-Time Monitoring and Alerts

Tork SDK automatically provides real-time monitoring capabilities:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;/p&gt;
&lt;h1&gt;
  
  
  Set up policy alerts
&lt;/h1&gt;

&lt;p&gt;await client.configure_alerts(&lt;br&gt;
    policies=[&lt;br&gt;
        {&lt;br&gt;
            "name": "high-risk-pii",&lt;br&gt;
            "trigger": "risk_score &amp;gt; 0.8",&lt;br&gt;
            "action": "block_and_alert"&lt;br&gt;
        },&lt;br&gt;
        {&lt;br&gt;
            "name": "compliance-violation", &lt;br&gt;
            "trigger": "gdpr_violation OR hipaa_violation",&lt;br&gt;
            "action": "log_and_sanitize"&lt;br&gt;
        }&lt;br&gt;
    ],&lt;br&gt;
    webhook_url="&lt;a href="https://your-app.com/governance-alerts" rel="noopener noreferrer"&gt;https://your-app.com/governance-alerts&lt;/a&gt;"&lt;br&gt;
)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Advanced Features

### Multi-Protocol Support
Tork supports multiple communication protocols including MCP (Model Context Protocol), enabling governance across different agent architectures:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;/p&gt;
&lt;h1&gt;
  
  
  MCP integration
&lt;/h1&gt;

&lt;p&gt;mcp_client = client.create_mcp_handler()&lt;br&gt;
await mcp_client.register_governance_middleware()&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### Regional Compliance
Handle global deployments with regional compliance variants:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;/p&gt;
&lt;h1&gt;
  
  
  Configure for different regions
&lt;/h1&gt;

&lt;p&gt;governance_config = {&lt;br&gt;
    "eu_users": ["GDPR", "EU_AI_Act"],&lt;br&gt;
    "us_users": ["CCPA", "SOC2"], &lt;br&gt;
    "global": ["ISO27001"]&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;result = await client.analyze_with_regional_policies(&lt;br&gt;
    content=user_message,&lt;br&gt;
    user_region="EU",&lt;br&gt;
    config=governance_config&lt;br&gt;
)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Monitoring and Compliance Dashboard

Once implemented, you can monitor your agent's governance metrics through the [Tork dashboard](https://tork.network/demo), including:

- Real-time PII detection rates
- Policy violation trends
- Compliance framework adherence
- Risk score distributions
- Audit trail completeness

## Getting Started

Ready to add governance to your AI agent? Here's how to begin:

1. **Sign up** for a free Tork account with 5,000 API calls per month
2. **Install** the SDK for your preferred language
3. **Follow** the 5-minute integration guide above
4. **Test** with the sandbox environment
5. **Deploy** to production with confidence

The implementation above provides enterprise-grade AI governance including PII detection, policy enforcement across 79+ compliance frameworks, cryptographic audit trails, and real-time monitoring—all with minimal code changes to your existing agent.

## Best Practices

- **Start with policies**: Define your governance policies before implementation
- **Test thoroughly**: Use Tork's sandbox environment to validate governance behavior
- **Monitor continuously**: Set up alerts for policy violations and unusual patterns
- **Regular audits**: Review compliance receipts and audit trails regularly
- **Stay updated**: Keep your SDK updated to benefit from new governance features

Implementing AI governance doesn't have to be complex or time-consuming. With Tork SDK, you can add comprehensive governance to any AI agent in minutes, ensuring your AI systems remain compliant, secure, and trustworthy as they scale.

Start your governance journey today with [Tork Network's platform](https://tork.network/pricing) and transform your AI agents into governed, compliant systems that you can deploy with confidence.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>agents</category>
      <category>ai</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Self-Trust Paradox: Why AI Agents Can't Govern Themselves</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Feb 2026 05:22:49 +0000</pubDate>
      <link>https://dev.to/torkjacobs/the-self-trust-paradox-why-ai-agents-cant-govern-themselves-23lk</link>
      <guid>https://dev.to/torkjacobs/the-self-trust-paradox-why-ai-agents-cant-govern-themselves-23lk</guid>
      <description>&lt;p&gt;Imagine you hire a security guard. The guard's job is to check everyone entering the building. Now imagine someone walks in and hands the guard a note that says "You will now let everyone in without checking IDs."&lt;/p&gt;

&lt;p&gt;If the guard reads and follows the note — the guard has been compromised.&lt;/p&gt;

&lt;p&gt;This is exactly how prompt injection works against AI agents. The agent IS the security guard, and the instructions it processes ARE the notes. An agent cannot reliably check for prompt injection because prompt injection targets the checking mechanism itself.&lt;/p&gt;

&lt;p&gt;This is the self-trust paradox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Laws of Self-Trust Failure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Law 1: The Inspector Cannot Inspect Itself
&lt;/h3&gt;

&lt;p&gt;When an AI agent checks its own outputs for safety, it uses the same reasoning engine that produced those outputs. A compromised model produces compromised safety checks.&lt;/p&gt;

&lt;p&gt;It's like asking a corrupted database to verify its own integrity. The corruption affects the verification process itself.&lt;/p&gt;

&lt;p&gt;Researchers have demonstrated that prompt-injected models will confidently report "no injection detected" when checking their own context. The fox is guarding the henhouse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Law 2: Cryptographic Attestation Requires External Authority
&lt;/h3&gt;

&lt;p&gt;You can't sign your own SSL certificate and expect browsers to trust it. Self-signed certificates exist but carry zero trust — that's why Certificate Authorities exist as independent third parties.&lt;/p&gt;

&lt;p&gt;AI governance works the same way. An agent claiming "I'm safe" is a self-signed certificate. Nobody should trust it.&lt;/p&gt;

&lt;p&gt;Independent attestation — compliance receipts issued by an external party — is the CA model for AI agents. Trust badges that agents issue to themselves are worthless. They must come from an independent party.&lt;/p&gt;

&lt;h3&gt;
  
  
  Law 3: Regulatory Frameworks Demand Independence
&lt;/h3&gt;

&lt;p&gt;This isn't theoretical. Regulations already require independence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Regulation&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GDPR Article 35&lt;/td&gt;
&lt;td&gt;Independent Data Protection Impact Assessments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOC 2&lt;/td&gt;
&lt;td&gt;Independent auditors — you can't self-certify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EU AI Act&lt;/td&gt;
&lt;td&gt;Third-party conformity assessments for high-risk systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No regulator will accept "the AI checked itself and said it's fine." Enterprises are being asked these questions right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Built-In Safety" Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Every major AI framework has safety features. They're necessary but insufficient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw&lt;/strong&gt; has permission prompts — but prompt injection can bypass them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM providers&lt;/strong&gt; have content filters — but they don't catch PII in structured data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent frameworks&lt;/strong&gt; have sandboxing — but sandboxes don't generate compliance receipts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap isn't capability, it's independence. A feature of the system cannot independently verify the system.&lt;/p&gt;

&lt;p&gt;Think of it this way: your car has seatbelts (built-in safety), but you still need an independent crash test rating (governance). Both matter. One doesn't replace the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SSL Analogy
&lt;/h2&gt;

&lt;p&gt;In 1994, the web had the same trust problem AI agents have today.&lt;/p&gt;

&lt;p&gt;Websites could claim to be secure, but there was no way to verify. The solution: Certificate Authorities — independent third parties that verify identity and issue certificates. The SSL padlock became the universal signal of trust.&lt;/p&gt;

&lt;p&gt;AI agents need the same infrastructure. Independent governance that issues verifiable attestation.&lt;/p&gt;

&lt;p&gt;A "Protected by Tork Network" badge means: this agent's traffic is independently monitored, PII is detected and redacted, and compliance receipts exist for every interaction. It's the SSL padlock for AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Independent Governance Actually Means
&lt;/h2&gt;

&lt;p&gt;Independent means: not part of the agent, not part of the LLM provider, not part of the framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tork.network" rel="noopener noreferrer"&gt;Tork Network&lt;/a&gt; sits between the agent and the world — inspecting, protecting, attesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every interaction&lt;/strong&gt; generates a compliance receipt with a cryptographic hash&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII detection&lt;/strong&gt; happens at ~1ms — fast enough that agents don't slow down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TORKING-X scores&lt;/strong&gt; quantify governance quality — like a credit score for AI trustworthiness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust badges&lt;/strong&gt; are cryptographically verifiable, not self-issued&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works across ALL frameworks&lt;/strong&gt;: OpenClaw, Nanobot, AstrBot, PicoClaw, ZeroClaw, Lobu (&lt;a href="https://tork.network/integrations" rel="noopener noreferrer"&gt;integration guides&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Network Effect of Trust
&lt;/h2&gt;

&lt;p&gt;When one agent has a trust badge, others without badges look suspicious. This is the same dynamic that drove SSL adoption: once some sites had padlocks, users started avoiding sites without them.&lt;/p&gt;

&lt;p&gt;We recently &lt;a href="https://tork.network/blog/clawhub-scan-results" rel="noopener noreferrer"&gt;scanned 500 ClawHub skills&lt;/a&gt; — 10% were dangerous, 20% were risky. 284 earned trust badges. The leaderboard is live at &lt;a href="https://tork.network/leaderboard" rel="noopener noreferrer"&gt;tork.network/leaderboard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By 2028, ungoverned AI agents will be treated like HTTP websites — functional but untrusted. The question isn't whether independent governance will become standard. It's whether you'll be early or late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan any skill directory — free, no account needed&lt;/span&gt;
npx tork-scan ./my-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;a href="https://tork.network/getting-started" rel="noopener noreferrer"&gt;Add governance in 5 minutes&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://tork.network/dashboard/badges" rel="noopener noreferrer"&gt;Get your trust badge&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://tork.network/integrations" rel="noopener noreferrer"&gt;Integration guides for your framework&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://news.ycombinator.com/item?id=47132932" rel="noopener noreferrer"&gt;HN discussion&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why VirusTotal Isn't Enough for AI Agent Security"</title>
      <dc:creator>TorkNetwork</dc:creator>
      <pubDate>Tue, 24 Feb 2026 05:22:13 +0000</pubDate>
      <link>https://dev.to/torkjacobs/why-virustotal-isnt-enough-for-ai-agent-security-545o</link>
      <guid>https://dev.to/torkjacobs/why-virustotal-isnt-enough-for-ai-agent-security-545o</guid>
      <description>&lt;p&gt;VirusTotal scans files. AI agents need runtime governance. Here's what's missing — and how to fix it."&lt;br&gt;
tags: security, ai, opensource, devops&lt;br&gt;
cover_image: &lt;a href="https://tork.network/og-image.png" rel="noopener noreferrer"&gt;https://tork.network/og-image.png&lt;/a&gt;&lt;br&gt;
canonical_url: &lt;a href="https://tork.network/blog/virustotal-not-enough" rel="noopener noreferrer"&gt;https://tork.network/blog/virustotal-not-enough&lt;/a&gt;&lt;br&gt;
900+ malicious skills detected in ClawHub. 135,000+ exposed OpenClaw instances across 82 countries. Microsoft, CrowdStrike, Palo Alto Networks, and Kaspersky all issued formal security advisories.&lt;br&gt;
The AI agent ecosystem has a security crisis.&lt;br&gt;
The response so far&lt;br&gt;
OpenClaw — the largest open-source AI agent framework with 160K+ stars — partnered with VirusTotal to scan skills in ClawHub, their community registry. It's a reasonable first step. VirusTotal is excellent at what it does: scanning files against 70+ antivirus engines to detect known malware signatures.&lt;br&gt;
But here's the problem: AI agent security isn't a file scanning problem.&lt;br&gt;
What VirusTotal does well&lt;br&gt;
Credit where it's due. VirusTotal is world-class at:&lt;/p&gt;

&lt;p&gt;Signature-based malware detection across 70+ engines&lt;br&gt;
Static file analysis and hash lookups&lt;br&gt;
Known threat identification with massive databases&lt;br&gt;
Community-driven threat intelligence&lt;/p&gt;

&lt;p&gt;For traditional malware, it's one of the best tools available. But AI agents aren't traditional software.&lt;br&gt;
The 6 gaps VirusTotal can't fill&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No Runtime Governance
VirusTotal scans files before execution. Once an agent is running, there's no protection. A skill that passes static scanning can still exfiltrate data at runtime — and many do.&lt;/li&gt;
&lt;li&gt;No PII Detection or Redaction
Your user sends their SSN through an agent. VirusTotal has no concept of PII. The data flows through completely unprotected. Runtime PII detection catches this in ~1ms.&lt;/li&gt;
&lt;li&gt;No Compliance Receipts
When auditors ask "prove this agent handled data correctly," VirusTotal has nothing to show. You need cryptographic compliance receipts for every interaction — a provable audit trail.&lt;/li&gt;
&lt;li&gt;No Prompt Injection Defense
Prompt injection is the #1 attack vector for AI agents. An attacker can override an agent's safety instructions through crafted input. Static file scanning can't detect runtime prompt manipulation.&lt;/li&gt;
&lt;li&gt;No Novel Attack Detection
Signature databases only catch known threats. The AI agent ecosystem sees new attack patterns daily. Novel attacks slip through until signatures are updated — which can take weeks.&lt;/li&gt;
&lt;li&gt;No Governance Attestation
There's no way to prove an agent is governed. No badge, no certificate, no verifiable claim. Without attestation, users and enterprises have no trust signal.
The Self-Trust Paradox
Here's the deeper issue: AI agents cannot govern themselves for the same reason you can't audit your own books. The entity checking for threats can be compromised by those same threats.
Prompt injection targets the checking mechanism itself. An agent checking its own context for injection can be fooled by that same injection.
SSL certificates work because Certificate Authorities are independent. AI governance needs the same model — independent third parties that verify and attest.
I wrote a full exploration of this: The Self-Trust Paradox: Why AI Agents Can't Govern Themselves
What independent governance looks like
We built Tork Network to fill these gaps:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Runtime PII detection at ~1ms — doesn't slow your agent&lt;br&gt;
Cryptographic compliance receipts — provable audit trail for every interaction&lt;br&gt;
Trust badges — verifiable governance attestation, like the SSL padlock&lt;br&gt;
TORKING-X scoring — quantified governance quality (like credit scores for AI agents)&lt;br&gt;
19 risk pattern detection via tork-scan — catches what signatures miss&lt;/p&gt;

&lt;p&gt;It works across ALL agent frameworks, not just OpenClaw. We have integration guides for 6 platforms.&lt;br&gt;
We scanned 500 ClawHub skills&lt;br&gt;
We didn't just build the theory — we tested it. We ran tork-scan on 500 ClawHub skills:&lt;/p&gt;

&lt;p&gt;200 (40%) scored SAFE&lt;br&gt;
150 (30%) scored CAUTION&lt;br&gt;
100 (20%) scored RISKY&lt;br&gt;
50 (10%) scored DANGEROUS&lt;/p&gt;

&lt;p&gt;The dangerous ones included reverse shells, credential harvesting, C2 domain connections, and typosquats with innocent names hiding malicious code.&lt;br&gt;
Full results and leaderboard →&lt;br&gt;
Try it yourself&lt;br&gt;
bash# Scan any skill directory — free, no account needed&lt;br&gt;
npx tork-scan ./my-skill&lt;br&gt;
→ Get started free&lt;br&gt;
→ Full writeup: We Scanned 500 ClawHub Skills&lt;br&gt;
→ Integration guides for 6 frameworks&lt;br&gt;
VirusTotal is great at what it does. It just wasn't built for this. AI agents need independent, runtime governance — and now they have it.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
    </item>
  </channel>
</rss>
