Skip to content

Intent to Instruction

The Intent to Instruction node looks up the intent label written by a preceding ClassifyIntent or ClassifyIntentLLM node in a Map-kind string storage, and attaches the matching value as a kind=Instruction slot to the current interaction. Changing string_storage to a name that cannot be loaded, or to empty, returns 3007 InvalidParamValue and leaves the previous map in force.

The attached instruction is then rendered into the prompt by a later Generate or Generate and Speak node's Mustache template via the {{#instructions}} loop or the {{slot.<name>}} direct lookup.

NodeType: IntentToInstruction.

How it works

  1. Reads IntentionComponent from the current interaction (written by an upstream Classify Intent or Classify Intent (LLM) node).
  2. Calls FindFirst(intentLabel) on the configured Map string storage.
  3. If a match is found, attaches SlotComponent{outputName, matchedText, SlotKind::Instruction, ...} and returns "default".
  4. If no IntentionComponent is present, or the key is not found in the map, logs a warning and returns "default" without attaching any component. A later Generate or Generate and Speak template renders the instructions loop as empty, producing a general answer.

The node always takes the "default" exit — it never blocks or branches. Missing intents and missing keys are handled silently (with a log warning) so the graph keeps running without hard failures.

Parameters

Storage

Param Type Default Range Structural Localizable Description
string_storage str empty — — — Storage file that maps intent labels to instruction text. Used instead of Inline Keys and Instructions.
inline_keys list[str] empty list — ✓ — Intent labels, paired by position with Instructions. Used when String Storage is empty.
instructions list[LocalizableText] empty list — ✓ — Instruction text for each label in Inline Keys, in the same order.

Output

Param Type Default Range Structural Localizable Description
output_name str empty — ✓ — Name later nodes use to read the chosen instruction (in their Input, or as the slot. tag). Empty = this node's name.

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
default default_exit Node to run next. Empty = end the turn.

Exit routes

Route Fires when
default Always — the node never branches. Set default_exit to the downstream Generate node name, or leave it empty to terminate the turn (END).

The "default" exit fires regardless of whether an instruction was found. When nothing is found the template's {{#instructions}} loop renders as empty.

Side effects

  • Attaches a kind=Instruction SlotComponent{name=outputName, text} to the current interaction when a matching key is found. The slot is rendered into the prompt by the downstream GenerateNode via:
    • {{#instructions}}{{text}}{{/instructions}} — iterates all attached instruction-kind slots.
    • {{slot.<name>}} — renders only the slot with the matching name.
  • When no instruction is attached (missing IntentionComponent or missing key), {{#instructions}} renders as empty and the model produces a general response.

Diagnostics

When enable_diagnostics = true, this node contributes to debug_info.nodes[].diagnostics. See Turn Diagnostics JSON for the envelope.

No model runs, so the node emits parameters and output only.

Key Meaning Absent when
parameters.string_storage Name of the Map-kind storage that was looked up. —
parameters.output_name Name given to the emitted component. —
output.intent The intent label read from the turn's IntentionComponent. No classification node wrote one.
output.found true when the key matched and the instruction was attached. —
output.text The instruction text that was attached. found is false.

found: false with a non-empty output.intent means the classifier produced a label the map has no entry for — a KB gap. An absent output.intent means no classification reached this node at all, which is an upstream routing problem instead. Check the classifier's own output.classification in the same payload.

Minimum working example

from tryll_client.graph import (
    GraphDescription, ClassifyIntentParams, IntentToInstructionParams,
    TransformParams, GenerateParams,
)

# Create a Map storage: key = intent label, value = instruction text
client.create_keyed_string_storage(
    name="npc_instructions",
    keys=["insult", "pleasing", "joking"],
    values=[
        "The user insulted you. Respond curtly and insult them back.",
        "The user is being polite. Show grudging satisfaction.",
        'The user told a joke. Laugh once ("Ha."), then refocus on duty.',
    ],
)

# The IntentToInstruction node is wired after a ClassifyIntent node
graph = (
    GraphDescription()
    .add_node("classify", ClassifyIntentParams(
        embedded_string_storage="intent_kb",
        embedding_model="All-MiniLM-L6-v2 (Q4_K_M)",
        metadata_field="intent",
        threshold=0.6,
        found_exit="pick",
        not_found_exit="respond",
    ))
    .add_node("pick", IntentToInstructionParams(
        string_storage="npc_instructions",
        output_name="tone",
        default_exit="compose",
    ))
    .add_node("compose", TransformParams(
        template="{{#instructions}}[Tone: {{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 = "intent_kb";
cp.embedding_model         = "All-MiniLM-L6-v2 (Q4_K_M)";
cp.metadata_field          = "intent";
cp.threshold               = 0.6f;
cp.found_exit              = "pick";
cp.not_found_exit          = "respond";

IntentToInstructionParamsT pp;
pp.string_storage = "npc_instructions";
pp.output_name    = "tone";
pp.default_exit   = "compose";

TransformParamsT tp;
tp.template_    = "{{#instructions}}[Tone: {{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 = "intent_kb",
        EmbeddingModel        = "All-MiniLM-L6-v2 (Q4_K_M)",
        MetadataField         = "intent",
        Threshold             = 0.6f,
        FoundExit             = "pick",
        NotFoundExit          = "respond",
    })
    .AddIntentToInstruction("pick", new TryllIntentToInstructionParams
    {
        StringStorage = "npc_instructions",
        OutputName    = "tone",
        DefaultExit   = "compose",
    })
    .AddTransform("compose", new TryllTransformParams
    {
        Template    = "{{#instructions}}[Tone: {{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("intent_kb");
CP->bOverrideEmbeddingModel = true;
CP->EmbeddingModel = TEXT("All-MiniLM-L6-v2 (Q4_K_M)");
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("npc_instructions");
PP->bOverrideOutputName = true;
PP->OutputName    = TEXT("tone");
PP->DefaultExit   = TEXT("compose");

UTryllTransformParams* TP = UTryllNodeParamsFactory::MakeTransformParams(this);
TP->bOverrideTemplate = true;
TP->Template    = TEXT("{{#instructions}}[Tone: {{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::AddIntentToInstruction(name, IntentToInstructionParamsT) — GraphDescription.h
  • Python: GraphDescription.add_node(name, IntentToInstructionParams(...)) — tryll_client.graph
  • Unity: TryllGraphBuilder.AddIntentToInstruction(name, new TryllIntentToInstructionParams{...}) — Runtime/Generated/TryllGraphBuilder.Nodes.cs
  • Unreal: AddIntentToInstructionNode(builder, name, UTryllIntentToInstructionParams*) — Generated/TryllGraphBuilder.Nodes.h
  • Unreal (Blueprint): author UTryllIntentToInstructionParams on a node description in a UTryllWorkflowAsset