$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people
~/wiki/patterns

Intent routing

[ pattern ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ]#patterns · routing · intent · classification · llm-orchestration

TL;DR Put Jev in front of your handlers. One call returns intent (Choice) and complexity (Score); code sends order_status to a plain database lookup, two intents to different specialist LLMs, and complex or low-confidence cases to a human. "The expensive resources only get invoked for the requests that actually need them."

Problem

From raw/docs/patterns__intent-routing.md:

Not every user request needs the same kind of handler. Some can be answered with a database lookup. Some need an LLM with domain-specific context. Some need a human.

If every inbound message goes through an expensive LLM just to discover what kind of message it is, you pay frontier-model cost and latency on requests a SELECT could have answered — and you still have to parse the LLM's answer to route on it.

Pattern

TypeSafe can sit in front of all of these as a fast, cheap classifier that determines which handler to invoke.

Two steps: one classification call, then a dispatch table in code. The classification call carries more than the intent — a second question about how hard the request is to resolve turns "which handler" into "which handler, and is automation safe here at all."

This is speculative fan-out (two questions, one call) with a confidence gate on top.

Implementation

The documented example is customer service routing: "Messages come in and need to be routed to the right handler. Rather than sending every message through an expensive LLM to figure out what kind of request it is, you classify first and route accordingly."

Step 1: classify intent and complexity

Questions, verbatim from the source (the customer message is the state; add "state": ... and "model": "jev-latest" for a complete request, see HTTP API: POST /v1/systemone and GET /v1/models):

{
  "intent": {
    "type": "choice",
    "instructions": "The primary intent of this customer message",
    "criteria": {
      "order_status": "Asking about an existing order",
      "product_question": "Asking about a product before buying",
      "return_exchange": "Wants to return or exchange something",
      "complaint": "Unhappy with experience, wants resolution"
    }
  },
  "complexity": {
    "type": "score",
    "instructions": "How complex is this request to resolve",
    "criteria": [
      "Simple lookup or standard procedure",
      "Requires some judgment or multi-step process",
      "Unusual situation, edge case, or escalation needed"
    ]
  }
}

Step 2: route to the optimal handler

def route_ticket(ticket_id, response):
    intent = response.answers["intent"]
    complexity = response.answers["complexity"]

    if intent.confidence < 0.5:
        # If we don't have enough confidence to classify, route to a human agent
        return route_to_human_agent(ticket_id)

    if intent.choice == "order_status":
        handle_order_status(ticket_id)

    elif intent.choice == "product_question":
        handle_with_llm(ticket_id, PRODUCT_SPECIALIST)

    elif intent.choice == "return_exchange":
        handle_with_llm(ticket_id, RETURNS_SPECIALIST)

    elif intent.choice == "complaint":
        low_confidence = complexity.confidence < 0.5
        # A higher complexity.score leans toward the "escalation needed" end of the scale.
        if complexity.score > 1 or low_confidence:
            # Too complex for safe automation, or we're not sure about the complexity; route to a human.
            route_to_human_agent(ticket_id)
        else:
            handle_with_llm(ticket_id, COMPLAINT_RESOLUTION)

The same router in TypeScript with @typesafe-ai/sdk 0.6.0 — adapted from the Python sample (not in upstream docs), same questions, thresholds, and branches. choice() / score() build the questions; answers carry choice / probabilities / confidence and score / confidence; see JavaScript/TypeScript SDK: install, client, choice/score/noul.

import { choice, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY

export async function routeTicket(ticketId: string, message: string) {
  const { answers } = await client.systemOne({
    state: message,
    questions: {
      intent: choice("The primary intent of this customer message", {
        order_status: "Asking about an existing order",
        product_question: "Asking about a product before buying",
        return_exchange: "Wants to return or exchange something",
        complaint: "Unhappy with experience, wants resolution",
      }),
      complexity: score("How complex is this request to resolve", [
        "Simple lookup or standard procedure",
        "Requires some judgment or multi-step process",
        "Unusual situation, edge case, or escalation needed",
      ]),
    },
  });

  const { intent, complexity } = answers;

  if (intent.confidence < 0.5) {
    // If we don't have enough confidence to classify, route to a human agent
    return routeToHumanAgent(ticketId);
  }

  if (intent.choice === "order_status") {
    handleOrderStatus(ticketId);
  } else if (intent.choice === "product_question") {
    handleWithLlm(ticketId, PRODUCT_SPECIALIST);
  } else if (intent.choice === "return_exchange") {
    handleWithLlm(ticketId, RETURNS_SPECIALIST);
  } else if (intent.choice === "complaint") {
    const lowConfidence = complexity.confidence < 0.5;
    // A higher complexity.score leans toward the "escalation needed" end of the scale.
    if (complexity.score > 1 || lowConfidence) {
      // Too complex for safe automation, or we're not sure about the complexity; route to a human.
      routeToHumanAgent(ticketId);
    } else {
      handleWithLlm(ticketId, COMPLAINT_RESOLUTION);
    }
  }
}

The source's summary:

One intent routes to deterministic code with no LLM involved. Two route to different specialist LLMs, each loaded with different context. One uses the complexity score to decide between an LLM and a human. TypeSafe handles the classification all in a single quick call; the expensive resources only get invoked for the requests that actually need them.

And on the second gate:

Note the additional confidence check on the complexity score. As discussed in Confidence, it is always important to consider the meaning of a low confidence score in the context of the system and the stakes of the decision.

Three things worth copying: complexity is asked speculatively (only the complaint branch reads it); uncertainty about the complexity is treated the same as high complexity, both meaning "a human should look"; and 0.5 appears twice as two independent policy constants, not one model constant.

When it fails

Variants

Related

Sources