Skip to content

feat(workflows): make the deterministic intent router real - #2501

Open
Hoang130203 wants to merge 1 commit into
Osmantic:mainfrom
Hoang130203:feat/n8n-intent-router-workflow
Open

feat(workflows): make the deterministic intent router real#2501
Hoang130203 wants to merge 1 commit into
Osmantic:mainfrom
Hoang130203:feat/n8n-intent-router-workflow

Conversation

@Hoang130203

Copy link
Copy Markdown

Summary

config/n8n/08-m4-deterministic-voice.json was a placeholder — manualTrigger

  • sticky note + "connections": {} — behind a featured catalog card
    advertising "Intent classification with deterministic routing".
Webhook POST /webhook/ods-intent  {utterance}
  -> Utterance Present? (IF)      false -> Return 400
  -> Classify Intent              llama-server, temperature 0, max_tokens 8
  -> Constrain To Allowed Set     normalise + whitelist
  -> Route By Intent (Switch)     smart_home | timer | question | unknown
  -> Return Intent
curl -X POST http://localhost:5678/webhook/ods-intent \
  -H 'Content-Type: application/json' \
  -d '{"utterance": "turn the kitchen lights off"}'

The "deterministic" part is built in, not prompted for

The card promises deterministic routing, so the workflow enforces it rather
than trusting the model to behave:

  • The classifier runs at temperature: 0 with max_tokens: 8 and is
    told to answer with a single lowercase word.
  • Its reply is then lowercased, stripped to [a-z_], and matched against the
    allowed set
    . A sentence, a made-up label, a chatty preamble, or an empty
    completion all normalise to unknown.
  • A Switch node — not the model — selects the branch, with a named
    fallback output for unknown.

That ordering matters: a hallucinated label cannot route a smart-home command
into a branch that actuates something. It lands in the branch that exists for
"I could not classify this". The raw label is returned alongside the
constrained one, so you can see when the model went off-script.

All four Switch outputs converge on the responder, so the workflow runs
correctly as shipped and you can watch classifications before wiring real
actions to the branches. A sticky note on the Switch says what each branch is
for.

AI Assistance

AI assisted with drafting the node graph, the classifier prompt, and this
description. I confirmed the Switch node's rule/fallback parameter shape
against the node definition (rules.valuesconditions, renameOutput,
outputKey; optionsfallbackOutput, renameFallbackOutput) rather than
from memory, and validated the file before pushing.

Release Lane

  • Stable hotfix targeting release/2.6.x
  • Mainline change targeting main
  • Next-minor work targeting the next feature/minor release
  • Not sure; reviewer should help classify

Stable hotfix reason:

n/a

Changed Surface

  • Docs only
  • Tests only
  • Dashboard UI
  • Dashboard API / host agent
  • Installer / bootstrap / lifecycle
  • Docker Compose / service manifests
  • Model routing / Hermes / capabilities
  • Network exposure / auth / proxy
  • Dependencies / runtime wiring

(One JSON file under config/n8n/. An import payload for n8n; no ODS code
executes it. The catalog entry is unchanged.)

Risk And Validation

  • Risk level: Low
  • Validation run:
    • git diff --check
    • Markdown/link sanity for docs
    • Focused tests listed below
    • Dashboard lint/test/build
    • Extension audit / compose validation
    • Release-grade fleet or scoped hardware validation
    • Stable-lane patch validation, if targeting release/2.6.x

Commands/results:

# Validated against the exact node package ODS ships (n8n 2.6.4 -> 2.6.2).

$ node verify.js 08-m4-deterministic-voice.json
n8n-nodes-base version: 2.6.2
node types loaded: 417
  checked 08-m4-deterministic-voice.json: 10 nodes

ALL WORKFLOWS VALID

# Switch parameter shape confirmed against the node definition:
#   switch versions: 1, 2, 3, 3.1, 3.2, 3.3, 3.4
#   props:   mode, numberOutputs, output, rules, looseTypeValidation, options
#   options: fallbackOutput, ignoreCase, looseTypeValidation,
#            renameFallbackOutput, allMatchingOutputs
#   rules.values: conditions, renameOutput, outputKey

Caveat: Docker is not running on my dev host, so I could not run a real
utterance through llama-server and watch the Switch fire. The validation is
static against the real node definitions. The normalisation and whitelist are
plain JS in a Set node, so their behaviour does not depend on the model — the
part I cannot demonstrate is the model's own label quality, which is exactly
what the whitelist exists to contain. Happy to get a live run before merge.

Operational Change Check

An import payload for n8n. Nothing in the installer, compose stack, ods-cli,
or dashboard-api executes it. No existing install changes until a user imports
it.

  • This is not an operational change.
  • This is an operational change and validation is recorded above.
  • This is an operational change and validation is intentionally deferred for:

Notes For Reviewers

The intent set is a placeholder I had to choose. smart_home, timer,
question, unknown are a reasonable voice-assistant starting set and match
what the card implies, but "M4" suggests this template came from a specific
milestone with its own intent taxonomy. If there is a canonical ODS intent list
I did not find, tell me and I will swap it — the shape of the workflow does not
change, only the strings in the prompt, the whitelist, and the Switch rules.

Branches deliberately converge. I could have left three outputs
disconnected to make "fill this in" obvious, but a workflow that half-runs on
import is worse than one that runs and returns its classification. The sticky
note carries the intent.

Part of the series making the 18 stub templates real: #2496, #2497, #2498,
#2499, #2500. Independent files, no overlapping lines.

config/n8n/08-m4-deterministic-voice.json was a manualTrigger plus a
sticky note saying "Customize the nodes below to match your setup", with
"connections": {} — an empty canvas behind a featured catalog card
advertising "Intent classification with deterministic routing".

Now it classifies and routes:

    POST /webhook/ods-intent {utterance}
      -> llama-server classify (temperature 0, max_tokens 8)
      -> constrain the answer to the allowed set
      -> Switch: smart_home | timer | question | unknown

The "deterministic" part is the point, so it is built in rather than
left to the prompt:

- The classifier runs at temperature 0 and is told to answer with one
  lowercase word.
- Its reply is lowercased, stripped to [a-z_], and matched against the
  allowed set. Anything else — a sentence, a made-up label, an empty
  completion — becomes `unknown`.
- A Switch node, not the model, selects the branch, with a named
  fallback output for unknown.

So a hallucinated label cannot route a smart-home command somewhere
unintended; it lands in the branch meant for "I could not classify this".

All four outputs converge on the responder so the workflow runs as
shipped and you can see the classification before wiring real actions. A
sticky note on the Switch says what each branch is for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant