npl-runtime
v0.4.1
Published
Simple NLP runtime: intent + multi-turn slots + WhatsApp button/list interactive.
Readme
npl-runtime
Simple NLP for chatbots: match intent → ask slots (with WhatsApp list/buttons) → confirm → onComplete.
License key (required)
createNplRuntime will not start without a valid licenseKey (or env NPL_LICENSE_KEY):
createNplRuntime({
licenseKey: 12098,
intents: [ /* ... */ ],
})Install
npm install npl-runtimeQuick start (with WhatsApp network list)
import { createNplRuntime, type CompleteContext } from 'npl-runtime';
async function fetchNetworks() {
return ['mtn', 'airtel', 'glo', '9mobile']; // or hit your API
}
const npl = await createNplRuntime({
licenseKey: 12098, // required
intents: [
{
name: 'buy_airtime',
examples: ['buy airtime', 'recharge', 'top up', 'vend'],
reply: 'Sure — I can help with airtime.',
slots: {
network: {
ask: 'Which network should I recharge?',
fetchOptions: async () => fetchNetworks(),
whatsapp: {
buttonText: 'Pick network',
sectionTitle: 'Available networks',
fetchItems: async () => fetchNetworks(),
},
},
amount: {
ask: 'How much?',
type: 'number',
min: 100,
max: 50000,
},
},
confirm: true,
onComplete: async ({ payload }: CompleteContext) => ({
ok: true,
message: `Purchased ${payload.amount} on ${payload.network}.`,
}),
},
],
});
const out = await npl.handle('I want to buy airtime', { sessionId: from });
// Prefer out.replies for separate WhatsApp bubbles (openers + ask)
const texts = out.replies?.length ? out.replies : out.reply ? [out.reply] : [];
const lastText = texts[texts.length - 1] ?? '';
for (const t of texts.slice(0, -1)) {
await sendText(from, t);
}
// out.whatsapp: { type: 'list', body, buttonText, sectionTitle, items: [...] }
// ≤3 options → type: 'button' | 4+ → type: 'list'
if (out.whatsapp?.type === 'list') {
await sendList(from, {
body: out.whatsapp.body,
buttonText: out.whatsapp.buttonText!,
sections: [{
title: out.whatsapp.sectionTitle,
rows: out.whatsapp.items.map((i) => ({
id: i.id,
title: i.title,
description: i.description,
})),
}],
});
} else if (out.whatsapp?.type === 'button') {
await sendButtons(from, {
body: out.whatsapp.body,
buttons: out.whatsapp.items.map((i) => ({ id: i.id, title: i.title })),
});
} else if (lastText) {
await sendText(from, lastText);
}Multiple bubbles (openerReplies / message: string[])
{
name: 'greetings',
utterances: ['hello', 'hi'],
openerReplies: [
'Welcome back *Yuki*! 👋\n\nWhat would you like to do today?',
'*Wallet balance:* ₦700\n\n*Your last Purchase:*\nMTN airtime - ₦200',
],
// or in onComplete: { message: ['bubble 1', 'bubble 2'] }
}When out.replies is set, send each string as its own WhatsApp message. Interactive (out.whatsapp) applies to the last bubble / ask only.
When the user taps a list row, pass list_reply.id (e.g. "mtn") back into handle.
Slot WhatsApp config (like NPL-Xel)
network: {
ask: 'Which network?',
fetchOptions: fetchNetworks, // for validation
whatsapp: {
buttonText: 'Pick network',
sectionTitle: 'Available networks',
fetchItems: fetchNetworks, // for the interactive UI
},
}Or static:
choices: ['mtn', 'airtel', 'glo', '9mobile'],
// package still builds out.whatsapp from choices if whatsapp/fetchOptions omittedRule
| Options | out.whatsapp.type |
|---|---|
| 1–3 | button |
| 4–10 | list |
Optional LLM (intentModel)
When intentModel is set (OpenAI, Ollama/local, etc.), the runtime builds prompts for you — you do not write custom prompts. You stay aligned by defining good intents and slots.
intentModel: {
provider: 'local', // or 'ollama' | 'openai' | ...
baseUrl: 'http://localhost:11434',
model: 'gemma4:e4b-mlx',
}What the model actually uses
1. Intent classification (pick buy_airtime, etc.)
Uses intent metadata, not slots:
- intent
name description/replyutterances/examples
Built into a classify prompt like: “Classify into one of these intents…”
2. Slot filling / confirm / edit (once intent is known)
Uses slots as the schema:
- slot names (
amount,network,phone) - types, min/max, choices
- current payload at confirm
Then the model is asked to extract values or interpret phrases like “edit phone / change amount…” against those fields.
So:
- Intent model ≠ slots for choosing intent
- Slots = what the model fills / edits after the intent is chosen
Define clear intent names + examples, and clear slot names/types/asks/choices. No custom prompt required.
Without intentModel, the runtime uses node-nlp + heuristics + parseSlotValue as before.
License
ISC
