dsh-intent-recognition
English | 中文
A DeepSeek Harness (DSH) plugin: five-layer intent recognition pipeline + scenario/intent management UI + dual-edition storage (basic = zero-config local file, advanced = PostgreSQL + pgvector) + distributable API services + DSH-native routing (agent tools / workflows / subagents / Cordis services).
User query
│
▼
┌─ L1 rule hard filter ───────── greeting / exit / command / keyword strong match (lexicon + regex + black/white lists)
│ │ miss
├─ L2 vector retrieval ───────── pgvector cosine search compresses the intent library to Top5–Top10 candidates
│ │
├─ L3 Jev probabilistic scoring ─ high confidence routes directly → mid → L4; low → L5
│ │
├─ L4 fine-tuned model ───────── BERT / small LLM arbitrates near-duplicate intents and boundary cases
│ │
└─ L5 LLM structured fallback ── multi-intent / cross-turn context / slot extraction / open queries, JSON output
Every layer can be toggled off per service — disabled layers are skipped entirely.
Control plane + data plane
- Control plane (management UI): the Intent Recognition sidebar panel (same level as the Plugins entry; pin/unpin from the sidebar's More menu), plus a Settings → Intent Recognition section — both render the same management UI. Tooling only; it never joins the conversation.
- Data plane (consumable services): a running service can be consumed two ways:
| Channel | Notes | Best for |
|---|---|---|
| Agent tool (recommended) | each running service registers intent_recognize_<service-slug>; high-confidence hits execute the route inside the tool |
DSH main agent routing user turns |
| Cordis service | ctx.intentRecognition.predict(service, query, context) for other plugins |
programmatic integration |
Features
- Zero-config start: the basic edition (local file storage) is the default — install and build intents, models and services without any database. Upgrade to the advanced edition in-page whenever you need vector retrieval; existing data migrates automatically.
- Intent management: per-scenario intent catalog (name/code/description/examples/keywords/blacklist/whitelist/route config), Excel import (merge/replace) & export, one-click embedding rebuild (advanced).
- Model configuration: embedding, Jev, fine-tuned and LLM models are all page-configured (OpenAI-compatible or plain http JSON endpoints), with connectivity tests.
- Service management: per-scenario API services, independent switches & parameters per layer, services can run / stop / delete / copy; per-service API keys; built-in test console and call logs.
- Vector lab (advanced): a dedicated tab for layer-2 recall debugging — recall tests (type a question, see per-chunk scores and the aggregated intent candidate set, optionally inheriting a service's threshold/TopK), chunk browsing (example text / dims / building model / timestamp, with single-chunk delete), and per-scenario coverage overview with in-place rebuild.
- Route execution: per-intent routes (none / DSH workflow / subagent / Cordis service) with a per-service
route_mode(auto vs suggest). - Dual-edition storage: basic = local JSON file (zero dependencies); advanced = PostgreSQL + pgvector (vectors and business data in one database), with in-page upgrade/downgrade.
- Agent integration: running services register/revoke
intent_recognize_<slug>tools automatically; optionally contributes a system-prompt section listing available intent services.
Tabs follow the order of use: Intents → Models → Services → Vector (advanced) → Database. Until the first service exists, a three-step onboarding strip sits on top of the page; in the basic edition the Vector tab carries a 🔒 and opens an upgrade explainer.
Screenshots
All shots below show the basic edition (local storage, zero config) in real use:
| Panel home (edition badge + 3-step onboarding) | Intent management (Excel bulk import) |
|---|---|
![]() |
![]() |
| Model configuration (connectivity test) | Service management (chat tool registered) |
|---|---|
![]() |
![]() |
| Test console (five-layer trace; L2 auto-skips in basic with a reason) | Agent tool call in a chat |
|---|---|
![]() |
![]() |
| Vector lab (advanced edition) | Database tab (advanced connection) |
|---|---|
![]() |
![]() |
Install
Prerequisites: Node.js ≥ 20, DSH installed (npm i -g @deepseek-ai/dsh). No database needed — the basic edition works out of the box.
Option A: dsh-market plugin marketplace (recommended)
Sidebar Plugins Marketplace → search dsh-intent-recognition → Install; reload the page when prompted and the Intent Recognition entry appears in the sidebar:
| Marketplace install | Installed (sidebar entry) |
|---|---|
![]() |
![]() |
Option B: npm registry / local package
# 1. Install the plugin
dsh plugin --profile web add dsh-intent-recognition
# or from a local checkout / tarball
dsh plugin --profile web add ./dsh-intent-recognition
dsh plugin --profile web add ./dsh-intent-recognition-0.7.1.tgz
# 2. Restart `dsh web` (desktop: restart the app or reload the page, Ctrl+R)
# —— client bundles load from the boot graph at page start; a reload is
# required after install before the sidebar entry appears
# 3. Click "Intent Recognition" in the left sidebar (or Settings → Intent Recognition)
# 4. Configure in tab order: Intents → Models → Services — that's all it takes
# 5. Need vector retrieval? Open the Database tab, save a PostgreSQL connection
# to upgrade — existing scenarios/intents/models/services migrate automatically
The desktop install flow from the official npm registry (Plugins → Add plugin → enter dsh-intent-recognition, source = npm official registry):
| Add plugin | Installing | Installed (running) |
|---|---|---|
![]() |
![]() |
![]() |
When to switch editions: start with the basic edition; upgrade on the Database tab when you need L2 vector retrieval. The switch is instant, local data migrates with ids and API keys preserved, and you can switch back to basic at any time — the local file stays untouched.
Desktop note: if the sidebar entry is missing after install/upgrade, reload the page first (Ctrl+R or restart the app). The page loads the plugin client roster once at boot — the official plugin manager shows the same "refresh" prompt after installs. If the entry is still missing after a reload, check Settings → Intent Recognition: both come from the same client bundle, and if neither renders the client half never loaded — confirm the component shows "running" on the plugin detail page.
The sidebar entry registers through DSH's
sidebar.panellist+mainpanel slots — the same mechanism as the built-in Plugins and Scheduled Tasks entries: clicking the icon opens the full management page in the main column, and the shell's More menu provides pin/unpin out of the box.
The package declares dsh.bundle.patch, so dsh plugin add mounts it into the profile bundle stack automatically — no manual cordis.patch.yml editing.
Editions: basic vs advanced
Since v0.7.0 the plugin ships two storage editions; a banner on first open explains which one is active:
| Basic (default) | Advanced | |
|---|---|---|
| Storage | Local JSON file (next to the connection config, e.g. data.json) |
PostgreSQL + pgvector |
| Database required | No — zero config | Yes (page-configured, instant effect) |
| L1 rules / L3 Jev / L4 fine-tuned / L5 LLM | ✅ | ✅ |
| L2 vector retrieval, vector lab, embedding rebuild | ❌ (tab shows 🔒; L2 is skipped at runtime with a trace note) | ✅ |
| Best for | trying things out, rule-driven catalogs | large intent libraries, diverse phrasings, semantic recall |
- Upgrade: fill in the PostgreSQL connection on the Database tab and "Test & Save". With "Migrate local data" on (default), existing scenarios/intents/models/services are copied into the database keeping ids and API keys, so agent tools and API callers notice nothing. Re-migration is idempotent (existing rows are skipped). Rebuild embeddings afterwards to enable L2.
- Downgrade: "Switch back to basic" on the Database tab returns storage to the local file — local data stays exactly as it was before the upgrade (rows created in advanced are not written back), and the connection config is kept for the next upgrade.
- Edition resolution (at boot): explicit basic choice > page-saved connection > patch
databaseUrl(only if reachable) > basic fallback. The plugin always loads and the panel always opens.
Database connection (advanced edition)
The "Intent Recognition → Database" tab manages the edition and the advanced connection: a "Storage edition" card on top shows the current edition, the local data file path, and per-table row counts. In the advanced edition it offers a structured form (host/port/database/user/password/SSL) or a raw DSN, Test connection (also probes pgvector availability), Test & Save (persists + hot-swaps storage without any restart), Clear page config (fall back to the patch DSN or basic), Switch back to basic, and Migrate local data into this database (available whenever local rows exist).
While the database is unreachable (advanced edition), a red banner at the top of the panel guides you to the Database tab; saving hot-swaps storage and takes effect immediately:
| Disconnected banner | Database tab (connected) |
|---|---|
![]() |
![]() |
- Priority: explicit basic choice > page-saved DSN >
cordis.patch.ymldatabaseUrl(only counts when reachable) > basic fallback. An unreachable page-saved DSN never silently falls back to basic (that would split your data) — the panel still opens, management routes answer 503, and you recover by fixing the connection or explicitly switching back to basic. - Persistence:
%APPDATA%/dsh-intent-recognition/(Windows),~/Library/Application Support/dsh-intent-recognition/(macOS),~/.config/dsh-intent-recognition/(Linux); override with theDSH_INTENT_RECOGNITION_CONFIGenv var. Inside:connection.json(advanced DSN; password masked as***in API responses, submitting***keeps the stored one),data.json(basic-edition data),storage.json(explicit basic marker). All files are mode 0600. - pgvector: with
autoMigrateon, a successful save runsCREATE EXTENSION IF NOT EXISTS vectorplus schema migration automatically; the test button warns beforehand when the target lacks pgvector. A database without pgvector can be connected, but L2 stays unavailable (the page says so). - Database maintenance: below the connection form, Check / Create / Drop database buttons manage the DSN's target database. Create does everything in one shot — create the database, install pgvector, and initialize all
ir_*tables. Drop is destructive: a red warning dialog requires typing the full database name to confirm, and system databases (postgres/template0/template1) are refused. Maintenance runs with the same credentials against the instance'spostgresmaintenance database, so it works even while the business database does not exist yet.
Plugin configuration
| Field | Default | Notes |
|---|---|---|
databaseUrl |
postgres://postgres:postgres@127.0.0.1:5432/dsh_intent |
Advanced-edition fallback DSN (page-saved config wins; only counts when reachable, otherwise basic edition); pgvector must be available |
embeddingDim |
1024 |
Must match the configured embedding model; changing it requires an embedding rebuild |
autoMigrate |
true |
Create tables/indexes on load (HNSW cosine index, pgvector ≥ 0.5) |
adminToken |
'' |
Bearer token protecting the management API; empty = DSH loopback only |
registerTool |
true |
Register one intent_recognize_<slug> tool per running service |
contributePrompt |
true |
Inject a system-prompt section listing available intent services |
Intent routes
Each intent may carry a route — "where to go next" once matched:
| Route type | Config | Behaviour |
|---|---|---|
none |
— | Return the recognition result only |
workflow |
target (run name) + workflow script | Starts a DSH workflow run; the script orchestrates freely (call subagents, tools, other plugins) |
subagent |
target = agent preset id | Spawns a subagent with that preset to handle the query (provider: spawn, result returned to caller) |
service |
target = serviceName.method |
Calls a method of another Cordis service in the host (capability provided by another plugin) |
Execution policy (route_mode)
- auto (default): deterministic hits (L1 / L3 / L4) execute the route inside the tool — the cheap deterministic path actually lands; L5 (LLM fallback) hits only return the route as a suggestion for the main agent.
- suggest: never executes; the route is returned to the caller (agent or HTTP client).
Results carry route when configured; after execution they also carry route_result (ok / value / error).
Workflow scripts
With route type workflow, write the script directly in the intent editor (a collapsible full guide is built into the page). The script receives:
| Variable | Content |
|---|---|
args.query |
raw user input |
args.intent |
matched intent code |
args.confidence |
0–1 |
args.matched_layer |
l1–l5 |
args.slots |
slots extracted by L5 |
args.context |
caller-provided context array |
agent(prompt) |
spawn a subagent inside the script |
meta |
run metadata (meta.name = route target) |
const intent = args.intent;
if (intent === 'query_balance') {
return await agent(`User wants their balance. Utterance: ${args.query}. Check and reply.`);
}
if (intent === 'transfer') {
const amount = args.slots?.amount ?? 'unknown amount';
return await agent(`User wants to transfer ${amount}. Utterance: ${args.query}. Confirm, then execute.`);
}
return `Unhandled intent: ${intent}`;
Runs are recorded with meta.name = <route target> (sanitized kebab-case) and are visible in DSH's run panel.
Plugging downstream capabilities into DSH
The whole system treats DSH as the backbone — intent routing and downstream agents/workflows all attach as plugins:
- Workflows: scripts live in the intent's route config and travel with the catalog via Excel import/export ("workflow script" column). Reuse capabilities via
agent()or route typeservice. - Subagents: ship downstream capabilities as agent preset plugins (see
@deepseek-ai/dsh-agent-preset); put the preset id in the route target. - Cordis services: a downstream plugin does
ctx.reflect.provide('xxxService', ...)inapply; set the route target toxxxService.methodName. The method is called as(result, exec)whereresultcarries query/intent/confidence/slots.
Using services in the chat window (Agent tools)
Once a service is running, the plugin registers a chat tool intent_recognize_<slug> with DSH (slug derived from the service name; revoked automatically when the service stops). From then on, just ask in any conversation — the main agent calls the tool when relevant and the route config dispatches to workflows / subagents / Cordis services. You can also invoke it explicitly, e.g. "use intent_recognize_bank_router to check: I want to transfer money".
Each service card shows a "chat tool registered / not registered" badge; if registration fails (e.g. a name clash), the reason is shown under the badge. State syncs in real time with service start/stop.
Note: tool registration is controlled by the
registerToolplugin option (on by default) and requires the DSHtoolsservice (present in web/desktop builds).
Jev extra-config auto-generation (systemone wire format)
The Jev model dialog offers two extra-config generators; pick a scenario whose intents are configured, then choose:
- AI generation: calls a configured LLM (add one under model configuration first; if none is configured or the call fails, the error is surfaced — use the rule template instead). The full intent descriptions (definition / inclusion signals / exclusion rules / boundary distinctions, with examples) are passed to the model verbatim, and the prompt demands faithful preservation of those rules.
- Rule template: deterministic, no LLM required — embeds each intent's full description into
instructionswith generic five-band scoring criteria.
Both generate all three judgment modes at once (modes.score / modes.choice / modes.noul). Since v0.6.0 each mode carries a single question named intent_recognition that judges every candidate intent in one call:
| Mode | type | criteria | answer | score mapping |
|---|---|---|---|---|
| score | score |
array of per-intent decision descriptions, plus intent_order (intent codes aligned 1:1 with criteria) |
score array (in intent_order order) or {code: score} object |
array indexes map via intent_order |
| choice | choice |
{intent name: decision description} object |
one intent name | chosen = 1, others = 0 |
| noul | noul |
— (map binds “是”/“否” to the two intent codes) |
“是”/“否” | per map: hit = 1, other = 0 |
noul is only generated for exactly-2-intent scenarios — yes/no cannot cover more; don't pick noul as the layer-3 mode otherwise.
Which mode runs is chosen per service: the service editor's layer 3 has a "judgment mode" dropdown (score/choice/noul, default score); layer 3 then sends the corresponding modes.<mode> question. Legacy extra configs (per-intent modes questions, or a single top-level questions) keep working — layer 3 falls back to per-intent calls.
Layer-3 thresholds decide the outcome: top score ≥ high threshold → direct hit (skips L4/L5); [mid, high) → compressed candidates go to L4 arbitration; below mid → L5 fallback. The result is a draft — review it in the extra-config field before saving.
A Jev model with this extra config (api_style=http) switches layer 3 to the systemone wire format:
POST {base_url}
{ "model": "<model>", "state": "<user input>", "questions": { "intent_recognition": <modes.<mode> question> } }
→ { "answers": { "intent_recognition": [0.9, 0.1] | "查询余额" | "是" } }
Score answers may be an array aligned with intent_order, a {scores|values|results: [...]} wrapper, or a {code: score} object; choice answers are an intent name (「」 quotes, codes, case and containment tolerant); noul answers are “是”/“否” mapped via the question's map. All three modes also accept object answers like {choice: "name", confidence: 1, probabilities: {name: p}} — probabilities maps are scored directly, otherwise the choice/answer/value label field is matched. Legacy per-intent answers {"answers": {"<intent code>": ...}} keep working: numbers, booleans, "是"/"否"/"true" strings, or {score|probability|confidence} objects.
Layer-2 retrieval transparency
The layer-2 "confidence threshold (min similarity)" (minScore) in the service editor is the slice filter — recalled chunks below it never become candidates. Test traces now expose it: layer 2 carries minScore / topK / chunks (every raw recalled slice with its score and pass/fail) / candidates (post-aggregation), so "recalled but not adopted" is directly attributable to threshold filtering vs aggregation truncation; layer 3 traces carry the judgment mode.
Vector lab (layer-2 debugging)
Advanced-edition feature (requires pgvector). In the basic edition the tab shows 🔒 with an upgrade explainer; available after upgrading and rebuilding embeddings.
The Vector Lab tab answers "why was recall wrong for this question":
- Recall test: pick a scenario, type any question, and the plugin replays the engine's real layer-2 logic (same
searchSimilar, per-intent best chunk, minScore filter, TopK truncation). Optionally reference a service to inherit its embedding model and layer-2 threshold/TopK, or pick a model and tweak the threshold ad hoc. Two result tables: the intent aggregation (what layer 2 would actually emit downstream) and the raw chunk hits (every recalled slice with its score). - Chunk browser: all embedding slices of a scenario (example text, owning intent, dims, building model, build time), filterable by intent, paginated, with per-chunk delete for stale slices.
- Coverage overview: per-scenario intent/example/chunk counts, coverage gaps, the models that built the slices, last rebuild time, and in-place rebuild with a chosen model.
Slices carry model provenance (model_id); after switching embedding models or editing examples, rebuild — otherwise old and new slices live in incompatible vector spaces.
Programmatic access
From another dsh plugin's apply(ctx):
const ir = ctx.get('intentRecognition');
if (ir) {
const result = await ir.predict('bank-router', 'check my balance', []);
}
Excel template
Header row: Name | Code | Description | Examples | Keywords | Blacklist | Whitelist | Route Type | Route Target | Workflow Script. Multi-values inside examples are newline-separated, others use 、; route type is none / workflow / subagent / service (a target or script without a type infers workflow). The exported file is the template.
Development
pnpm install
pnpm typecheck
pnpm build # emits lib/index.js + lib/client.js (esbuild)
node test/smoke.mjs # end-to-end pipeline (needs local PostgreSQL+pgvector, see test/README.md)
node test/basic-mode.mjs # basic-edition lifecycle: local CRUD/predict/Excel → upgrade migration → downgrade (needs local PostgreSQL)
node test/cordis-load.mjs # real Cordis host integration (tool registration / route dispatch / prompt)
node test/connection.mjs # edition resolution: basic fallback, advanced-degraded 503 guard, downgrade recovery (no DB needed)
node test/client-contract.mjs # client bundle slot-registration contract (no browser needed)
Source layout: src/index.ts (host entry / Cordis integration), src/pipeline/engine.ts (five-layer engine), src/dispatcher.ts (route dispatch: workflow/subagent/service), src/service-tools.ts (per-service tool registration), src/routes.ts (REST API), src/store.ts (storage interface + basic-edition FileStore), src/storage.ts (edition management: resolution / upgrade migration / downgrade), src/db.ts (advanced PostgreSQL/pgvector, hot-swappable pool), src/connection.ts (page-managed connection), src/models.ts (model calls), src/excel.ts (import/export), src/client/ (React management UI, settings.section + main + sidebar.panellist slots).
License
MIT














No comments yet. Be the first to write one.