Match queries to intents
Classify a query into intents, read scores and targets, and tune thresholds and result limits.
Matching is the step where Intent Router does its job at runtime. After you create a Router and give it intents with example phrases, your application sends each incoming user message to match. The Router returns the closest intents, each with a score and a target. Your application uses the top result to decide what happens next, such as sending an order question to an order-tracking agent and a refund request to a refunds workflow.
The examples use the connection values from the quickstart and match against its track_order and refund intents. Each complete program creates those intents first, so it needs an agent key with write access. Matching on its own needs only read access. Choose one language tab and use it for the whole page.
Send a query
A match request takes a query and returns the closest intents, best first.
for item in router.match("where is my order please", limit=3, min_score=0.6):
print(item.name, item.score, item.target)
from redis_iris.intent_router import IntentRouter, Target
router = IntentRouter.from_env()
router.define(
"track_order",
description="The customer wants to know where an order is.",
target=Target("tool", "orders.track", {"region": "eu"}),
examples=["where is my order", "track my package", "has my parcel shipped"],
)
router.define(
"refund",
examples=["i want a refund", "refund my purchase", "i would like my money back"],
)
for item in router.match("where is my order please", limit=3, min_score=0.6):
print(item.name, item.score, item.target)
def route(query: str) -> str:
top = router.match(query).best
if top is None:
return "no intent is close enough: use a default path"
if top.target is None:
return f"none {top.name}"
return f"{top.target.type} {top.target.ref}"
print(route("where is my order please"))
for item in router.match("i need help", min_score=0):
print(item.name, round(item.score, 2))
const { result } = await router.match({
query: "where is my order please",
limit: 3,
minScore: 0.6,
});
for (const item of result.items) console.log(item.name, item.score, item.target);
import { IntentRouter } from "@redis-iris/intent-router";
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}`);
return value;
}
const router = new IntentRouter({
serverURL: requiredEnv("IRIS_INTENT_ROUTER_URL"),
routerId: requiredEnv("IRIS_INTENT_ROUTER_ID"),
apiKey: requiredEnv("IRIS_INTENT_ROUTER_API_KEY"),
retryConfig: { strategy: "none" },
});
await router.upsertIntent("track_order", undefined, undefined, {
description: "The customer wants to know where an order is.",
target: { type: "tool", ref: "orders.track", args: { region: "eu" } },
examples: ["where is my order", "track my package", "has my parcel shipped"],
});
await router.upsertIntent("refund", undefined, undefined, {
examples: ["i want a refund", "refund my purchase", "i would like my money back"],
});
const { result } = await router.match({
query: "where is my order please",
limit: 3,
minScore: 0.6,
});
for (const item of result.items) console.log(item.name, item.score, item.target);
async function route(query: string): Promise<string> {
const { items } = (await router.match({ query })).result;
if (items.length === 0) return "no intent is close enough: use a default path";
const top = items[0];
if (!top.target) return `none ${top.name}`;
return `${top.target.type} ${top.target.ref}`;
}
console.log(await route("where is my order please"));
const { result: candidates } = await router.match({ query: "i need help", minScore: 0 });
for (const item of candidates.items) console.log(item.name, item.score.toFixed(2));
ROUTER_API="${IRIS_INTENT_ROUTER_URL%/}/iris/data/v1/intent/routers/$IRIS_INTENT_ROUTER_ID"
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"query": "where is my order please", "limit": 3, "minScore": 0.6}' \
| jq -c '.items[] | {name, score, target}'
set -euo pipefail
SEED_API="${IRIS_INTENT_ROUTER_URL%/}/iris/data/v1/intent/routers/$IRIS_INTENT_ROUTER_ID"
curl --fail-with-body --silent --show-error -o /dev/null \
-X PUT "$SEED_API/intents/track_order" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"description": "The customer wants to know where an order is.",
"target": {"type": "tool", "ref": "orders.track", "args": {"region": "eu"}},
"examples": ["where is my order", "track my package", "has my parcel shipped"]
}'
curl --fail-with-body --silent --show-error -o /dev/null \
-X PUT "$SEED_API/intents/refund" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"examples": ["i want a refund", "refund my purchase", "i would like my money back"]}'
ROUTER_API="${IRIS_INTENT_ROUTER_URL%/}/iris/data/v1/intent/routers/$IRIS_INTENT_ROUTER_ID"
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"query": "where is my order please", "limit": 3, "minScore": 0.6}' \
| jq -c '.items[] | {name, score, target}'
QUERY="where is my order please"
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg query "$QUERY" '{query: $query}')" > match.json
if [ "$(jq '.items | length' match.json)" -eq 0 ]; then
echo "no intent is close enough: use a default path"
else
jq -r '.items[0] | if .target then "\(.target.type) \(.target.ref)" else "none \(.name)" end' match.json
fi
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"query": "i need help", "minScore": 0}' | jq -c '.items[] | {name, score}'
iris intent-router match "where is my order please" --limit 3 --min-score 0.6
set -euo pipefail
iris intent-router intents add track_order \
--description "The customer wants to know where an order is." \
--target '{"type": "tool", "ref": "orders.track", "args": {"region": "eu"}}' \
--example "where is my order" --example "track my package" --example "has my parcel shipped" \
> /dev/null
iris intent-router intents add refund \
--example "i want a refund" --example "refund my purchase" --example "i would like my money back" \
> /dev/null
iris intent-router match "where is my order please" --limit 3 --min-score 0.6
iris --json intent-router match "where is my order please" > match.json
if [ "$(jq '.items | length' match.json)" -eq 0 ]; then
echo "no intent is close enough: use a default path"
else
jq -r '.items[0] | if .target then "\(.target.type) \(.target.ref)" else "none \(.name)" end' match.json
fi
iris intent-router match "i need help" --min-score 0
| Field | Required | Meaning |
|---|---|---|
query |
Yes | The text to classify, 1 to 2,048 characters. |
limit |
No | The most intents to return. Defaults to the deployment default, which is 5. |
minScore |
No | The minimum score for every returned intent, from 0 to 1. Overrides each intent's own and the Router's minScore. |
Each item in the response has:
| Field | Meaning |
|---|---|
name |
The intent's name. |
score |
How close the query is to the intent, from 0 to 1. Higher is closer. |
description |
The intent's description, when it has one. |
target |
The intent's target, when it has one. |
metadata |
The intent's metadata, when it has any. |
A query with no close intent returns 200 with an empty list.
Act on the result
The Router returns the target and leaves the action to your application. A typical handler takes the top item, checks that one exists, and dispatches on target.type.
def route(query: str) -> str:
top = router.match(query).best
if top is None:
return "no intent is close enough: use a default path"
if top.target is None:
return f"none {top.name}"
return f"{top.target.type} {top.target.ref}"
print(route("where is my order please"))
async function route(query: string): Promise<string> {
const { items } = (await router.match({ query })).result;
if (items.length === 0) return "no intent is close enough: use a default path";
const top = items[0];
if (!top.target) return `none ${top.name}`;
return `${top.target.type} ${top.target.ref}`;
}
console.log(await route("where is my order please"));
QUERY="where is my order please"
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg query "$QUERY" '{query: $query}')" > match.json
if [ "$(jq '.items | length' match.json)" -eq 0 ]; then
echo "no intent is close enough: use a default path"
else
jq -r '.items[0] | if .target then "\(.target.type) \(.target.ref)" else "none \(.name)" end' match.json
fi
iris --json intent-router match "where is my order please" > match.json
if [ "$(jq '.items | length' match.json)" -eq 0 ]; then
echo "no intent is close enough: use a default path"
else
jq -r '.items[0] | if .target then "\(.target.type) \(.target.ref)" else "none \(.name)" end' match.json
fi
Decide in advance what your application does when the list is empty, such as asking a clarifying question or using a general-purpose agent. When two items score close together, the query may be ambiguous: consider asking the user instead of picking the first.
Control relevance and result count
By default an intent's score is the similarity of its closest example to the query. The data plane reads the nearest examples, 50 by default, groups them by intent, and scores each group.
The minimum score for each intent comes from the first of these that is set:
- The
minScorein the match request, which applies to every intent. - The intent's own
minScore, when it is above0. - The Router's
minScore, which is0.7unless you set another.
Higher scores are stricter. Raise a threshold to reduce wrong matches and lower it to reduce misses.
| Goal | Adjust |
|---|---|
| One intent matches too easily | Raise that intent's minScore. |
| One intent misses real queries | Add examples that use the missed wording. Lower minScore only if examples don't help. |
| Every intent is too strict or too loose | Change the Router's minScore. |
| Compare scores while tuning | Match with minScore set to 0 to see the closest intents regardless of threshold, then pick thresholds. |
for item in router.match("i need help", min_score=0):
print(item.name, round(item.score, 2))
const { result: candidates } = await router.match({ query: "i need help", minScore: 0 });
for (const item of candidates.items) console.log(item.name, item.score.toFixed(2));
curl --fail-with-body --silent --show-error \
-X POST "$ROUTER_API/match" \
-H "Authorization: Bearer $IRIS_INTENT_ROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"query": "i need help", "minScore": 0}' | jq -c '.items[] | {name, score}'
iris intent-router match "i need help" --min-score 0
limit can't exceed the deployment cap, which defaults to 50. A request above the cap is rejected with 400 rather than clamped, so you never silently receive fewer ranked results than you asked for.
Scores aren't comparable across embedding models. A Router's model can't change, so recheck thresholds when you move to a Router with a different model.
Handle errors
Match is a data-plane request, so it fails when its Router can't serve:
400: the query is empty or too long, orlimitexceeds the cap.401: the key is invalid, expired, or revoked.403: the key has noreadgrant on the Router.409: the Router is provisioning or not serving. Retry after a short wait.423: an administrator suspended the Router.424: the embedding provider is unavailable. Retry with backoff.503withRetry-After: the Router's search index is rebuilding, or its database isn't answering. Retry after the interval. A Router that can't search never returns an empty list in place of an error.
See service limits and errors for all error types and retry guidance.