Classify Intent¶
The Classify Intent node maps a user message to a discrete intent label
by performing top-1 vector similarity search over a small, labelled
embedded string storage. When a label is
found it attaches an IntentionComponent to the current
interaction so a downstream
IntentToInstruction node can branch the graph
or look up a matching instruction. A filter that does not compile, changed
via ChangeAgentParam, returns 3007 InvalidParamValue and leaves the
previous filter in force.
It does not generate text and does not render anything into the
prompt — the IntentionComponent is routing-only; it is consumed by
IntentToInstructionNode and not by DefaultProjectionStrategy.
NodeType: ClassifyIntent.
How it works¶
- Reads the
input-resolved slot (empty = theuser_messageslot). - Embeds it with the embedding model recorded in the KB config file.
- Calls
Search(queryVector, topK=1, predicate)on the storage — using the same compiled-filter pipeline as Retrieve. - If the top hit's cosine distance is ≤
threshold, reads the configuredmetadata_fieldfrom that record's typed metadata. - Attaches
IntentionComponent{intentLabel}to the interaction and returnsfound. Otherwise returnsnot_found.
The knowledge base records must carry a string-typed metadata field whose value is the intent label. See KB schema requirements.
Parameters¶
Input¶
| Param | Type | Default | Range | Structural | Localizable | Description |
|---|---|---|---|---|---|---|
input |
str | empty | — | ✓ | — | Text to classify: the player's message or an earlier node's output. Empty = the player's message. |
Storage¶
| Param | Type | Default | Range | Structural | Localizable | Description |
|---|---|---|---|---|---|---|
embedded_string_storage |
str | empty | — | ✓ | — | Embedded file of example phrases, each labelled with its intent. |
metadata_field |
str | empty | — | — | — | Metadata field in that file that holds each example's intent label. |
filter |
str | empty | — | — | — | JSON filter on example metadata; only matching examples are compared. Empty = no filter. |
Behavior¶
| Param | Type | Default | Range | Structural | Localizable | Description |
|---|---|---|---|---|---|---|
threshold |
float | 0.25 | 0.0 – 2.0 | — | — | Max cosine distance of the closest example (0 = identical, 2 = opposite). Farther = Not Found. |
Output¶
| Param | Type | Default | Range | Structural | Localizable | Description |
|---|---|---|---|---|---|---|
notify_client |
NotifyClient | NotifyClient.Disabled | — | — | — | When to send the game an intent_classified event: Disabled, OnFound (Found exit only), or Always. |
Advanced¶
| Param | Type | Default | Range | Structural | Localizable | Description |
|---|---|---|---|---|---|---|
diagnostic_topk |
int | 5 | 1.0 – 64.0 | — | — | How many closest examples to report in diagnostics. Routing always uses the closest one. |
Exits¶
Each exit is a structural string field on the node's params; its value names the target node (empty = END).
| Exit | Param field | Description |
|---|---|---|
found |
found_exit |
Node to run when the closest example is within Threshold. Empty = end the turn. |
not_found |
not_found_exit |
Node to run when no example is close enough. Empty = end the turn. |
Exit routes¶
| Route | Fires when |
|---|---|
found |
Top-1 hit's cosine distance is less than or equal to threshold and its metadata_field contained a non-empty string. IntentionComponent is attached. |
not_found |
Resolved input slot missing/empty, empty storage, top-1 distance above threshold, or matched record's metadata_field is absent or empty. No component is attached. |
An empty exit field routes to END and is valid. A non-empty exit that does not
name a node in the graph fails agent creation with
3003 GraphCompilationFailed.
Side effects¶
- Embeds the
input-resolved text (empty =user_message) using the embedding model recorded in the KB config file referenced byembedded_string_storage. - Attaches an
IntentionComponent{intentLabel}to the current interaction onfound. The component is not rendered into the prompt byDefaultProjectionStrategy— it is consumed only byIntentToInstructionNode. - On
not_found, nothing is attached (a warning is logged if the top hit was rejected for a metadata reason rather than a distance reason). - When
notify_clientisOnFoundorAlways, fires oneNodeEvent(event_type="intent_classified") out-of-band before the turn completes.OnFoundemits only on thefoundpath;Alwaysalso emits on everynot_foundpath (empty_message,no_results,below_threshold) withnot_found_reasonset and the rejected top-1 fields when a hit exists. Subscribe on the agent:agent.set_on_intent_classified(cb)(Python),agent.SetOnIntentClassified(cb)(C++),TryllAgentComponent.IntentClassified(Unity), orUTryllAgentComponent::OnIntentClassified(Unreal). The session events (TryllClient.IntentClassified,UTryllSubsystem::OnIntentClassified) are the all-agents form(agentId, classification). The payload has no agent id. A typed subscriber swallows the genericOnNodeEventfallback in the same scope only.
KB schema requirements¶
The storage's *.json config must declare a fields array with at least
one entry whose type is "string" — this is the field you reference
in the metadata_field parameter:
{
"version": 1,
"embedding_model": "All-MiniLM-L6-v2 (Q4_K_M)",
"records_file": "my_intents.kb.json",
"index_file": "my_intents.usearch",
"fields": [
{ "name": "intent", "type": "string", "default": "" }
]
}
Each record in my_intents.kb.json carries the label in that field:
[
{ "id": "ex1", "text": "Move aside, you fool.", "metadata": { "intent": "insult" } },
{ "id": "ex2", "text": "May I please pass through?", "metadata": { "intent": "pleasing" } }
]
Diagnostics¶
When enable_diagnostics = true, this node contributes to
debug_info.nodes[].diagnostics. See
Turn Diagnostics JSON for the envelope.
The shared turn inspector renders this payload as the Intent block. Dialog Lab can also assert the accepted label with an Intent is / Intent not found check — see Compare dialog variants. Raw JSON remains the escape hatch for every field below.
| Key | Meaning | Absent when |
|---|---|---|
parameters.embedded_string_storage |
The knowledge base searched. | — |
parameters.input |
The configured input slot name. | Using the user_message default. |
parameters.metadata_field |
The metadata field read off the winning record to produce the label. | — |
parameters.threshold |
Max cosine distance accepted, as a number. | — |
parameters.notify_client |
Disabled, OnFound or Always. |
— |
parameters.diagnostic_topk |
How many ranked candidates to record below. | — |
parameters.filter |
The active filter, as JSON. Empty string when none. | — |
input.query |
The input-resolved text that was embedded. |
The resolved text was empty. |
output.classification.topk[] |
Ranked candidates: rank (0-based), intent, distance. Several entries can share one intent when a KB has multiple example phrases per label. |
diagnostic_topk is 0, or nothing was retrieved. |
output.classification.winner |
The match that was attached: intent, id, text, index (0-based record index), distance. |
The node exited not_found. |
output.classification.rejected_intent |
The top-1 label the search picked, when routing rejected it. | found, or there was no pick (empty_message, no_results). |
output.classification.rejected_distance |
That hit's raw distance. | Same as rejected_intent. |
output.classification.runner_up |
The best different intent: intent, distance, margin (its distance minus the winner's). |
There was no second distinct intent. |
output.classification.not_found_reason |
Why not_found fired. |
The node found a match, or recorded no reason. |
runner_up.margin is the number to watch when a classifier is unstable: a margin near zero
means the two labels were nearly tied, and small rewordings will flip the route.
notify_client does not gate this block
notify_client decides whether the client receives an intent_classified event. A
classification missing from the Agent Log is still recorded here in full.
Minimum working example¶
from tryll_client.graph import (
GraphDescription, ClassifyIntentParams, IntentToInstructionParams,
TransformParams, GenerateParams,
)
# 1. Create intent KB (file-backed, Path A)
client.create_embedded_string_storage(
name="intent_kb",
config_path="data/intents/intents.json",
)
# 2. Create instruction map (Map kind)
client.create_keyed_string_storage(
name="intent_instructions",
keys=["insult", "pleasing"],
values=[
"The user insulted you — respond curtly.",
"The user is being polite — acknowledge grudgingly.",
],
)
# 3. Build the graph
graph = (
GraphDescription()
.add_node("classify", ClassifyIntentParams(
embedded_string_storage="guard_intents/guard_intents.json",
metadata_field="intent",
threshold=0.6,
found_exit="pick",
not_found_exit="respond",
))
.add_node("pick", IntentToInstructionParams(
string_storage="intent_instructions",
output_name="tone",
default_exit="compose",
))
.add_node("compose", TransformParams(
template="{{#instructions}}[{{text}}]\n{{/instructions}}{{user_message}}",
default_exit="respond",
))
.add_node("respond", GenerateParams(
input="compose",
default_exit="", # empty = END
))
.set_start_node("classify")
.set_default_model_name("My Local Model")
)
agent = client.create_agent(graph)
using namespace Tryll::Client;
using namespace Tryll::NodeParams;
ClassifyIntentParamsT cp;
cp.embedded_string_storage = "guard_intents/guard_intents.json";
cp.metadata_field = "intent";
cp.threshold = 0.6f;
cp.found_exit = "pick";
cp.not_found_exit = "respond";
IntentToInstructionParamsT pp;
pp.string_storage = "intent_instructions";
pp.output_name = "tone";
pp.default_exit = "compose";
TransformParamsT tp;
tp.template_ = "{{#instructions}}[{{text}}]\n{{/instructions}}{{user_message}}";
tp.default_exit = "respond";
GenerateParamsT gp;
gp.input = "compose";
// gp.default_exit = ""; // empty = END (the default)
GraphDescription graph;
graph.AddClassifyIntent("classify", std::move(cp))
.AddIntentToInstruction("pick", std::move(pp))
.AddTransform("compose", std::move(tp))
.AddGenerate("respond", std::move(gp))
.SetStartNode("classify")
.SetDefaultModelName("My Local Model");
auto agent = client.CreateAgent(graph);
using Tryll.Client;
var graph = new TryllGraphBuilder()
.AddClassifyIntent("classify", new TryllClassifyIntentParams
{
EmbeddedStringStorage = "guard_intents/guard_intents.json",
MetadataField = "intent",
Threshold = 0.6f,
FoundExit = "pick",
NotFoundExit = "respond",
})
.AddIntentToInstruction("pick", new TryllIntentToInstructionParams
{
StringStorage = "intent_instructions",
OutputName = "tone",
DefaultExit = "compose",
})
.AddTransform("compose", new TryllTransformParams
{
Template = "{{#instructions}}[{{text}}]\n{{/instructions}}{{user_message}}",
DefaultExit = "respond",
})
.AddGenerate("respond", new TryllGenerateParams
{
Input = "compose",
})
.SetStartNode("classify")
.SetDefaultModelName("My Local Model")
.Build();
#include "Generated/TryllGraphBuilder.Nodes.h"
#include "Generated/TryllNodeParamsFactory.h"
UTryllClassifyIntentParams* CP = UTryllNodeParamsFactory::MakeClassifyIntentParams(this);
CP->bOverrideEmbeddedStringStorage = true;
CP->EmbeddedStringStorage.Path = TEXT("guard_intents/guard_intents.json");
CP->bOverrideMetadataField = true;
CP->MetadataField = TEXT("intent");
CP->Threshold = 0.6f;
CP->FoundExit = TEXT("pick");
CP->NotFoundExit = TEXT("respond");
UTryllIntentToInstructionParams* PP = UTryllNodeParamsFactory::MakeIntentToInstructionParams(this);
PP->bOverrideStringStorage = true;
PP->StringStorage.Path = TEXT("intent_instructions");
PP->bOverrideOutputName = true;
PP->OutputName = TEXT("tone");
PP->DefaultExit = TEXT("compose");
UTryllTransformParams* TP = UTryllNodeParamsFactory::MakeTransformParams(this);
TP->bOverrideTemplate = true;
TP->Template = TEXT("{{#instructions}}[{{text}}]\n{{/instructions}}{{user_message}}");
TP->DefaultExit = TEXT("respond");
UTryllGenerateParams* GP = UTryllNodeParamsFactory::MakeGenerateParams(this);
GP->bOverrideInput = true;
GP->Input = TEXT("compose");
FTryllGraphDescription Graph = FTryllGraphBuilder()
.AddNode(TEXT("classify"), CP)
.AddNode(TEXT("pick"), PP)
.AddNode(TEXT("compose"), TP)
.AddNode(TEXT("respond"), GP)
.SetStartNode(TEXT("classify"))
.SetDefaultModelName(TEXT("My Local Model"))
.Build();
See the end-to-end walkthrough in How to build an intent-driven NPC.
Client bindings¶
- C++:
GraphDescription::AddClassifyIntent(name, ClassifyIntentParamsT)—GraphDescription.h - Python:
GraphDescription.add_node(name, ClassifyIntentParams(...))—tryll_client.graph - Unity:
TryllGraphBuilder.AddClassifyIntent(name, new TryllClassifyIntentParams{...})—Runtime/Generated/TryllGraphBuilder.Nodes.cs - Unreal:
AddClassifyIntentNode(builder, name, UTryllClassifyIntentParams*)—Generated/TryllGraphBuilder.Nodes.h - Unreal (Blueprint): author
UTryllClassifyIntentParamson a node description in aUTryllWorkflowAsset
Related¶
- Intent to Instruction — the natural downstream node.
- Embedded String Storage
- String Storage — for the
Mapkind used byIntentToInstruction. - How to build an intent-driven NPC
- Retrieve — similar architecture; top-K document retrieval.
- Glossary