Compare commits

...

25 Commits

Author SHA1 Message Date
Hoan HL
faa6db9e74 wip 2026-03-16 17:42:48 +07:00
Hoan HL
554354d3c0 Merge branch 'main' of https://github.com/jambonz/verb-specifications into fix/gh_14 2026-03-16 17:00:01 +07:00
Hoan HL
b8523db008 wip 2026-03-16 16:59:42 +07:00
Dave Horton
11a0dce5ba 0.1.2 2026-03-15 21:32:54 -04:00
Dave Horton
ecf59041c5 add autogeneratePrompt to recognizer, new assemblyai v3 options 2026-03-15 21:32:54 -04:00
Dave Horton
abcb3ffa5d 0.1.1 2026-03-15 19:53:53 -04:00
Dave Horton
f5e7adcf66 update assemblyai 2026-03-15 19:53:36 -04:00
Dave Horton
f29b214208 0.1.0 2026-03-05 11:27:28 -05:00
Dave Horton
2bf7561980 better handling of synonyms and shortcuts (#127) 2026-03-05 11:26:51 -05:00
Dave Horton
96db93fcdc 0.0.130 2026-03-05 07:28:56 -05:00
Hoan Luu Huu
142718104a support model ces for dialogflow (#126) 2026-03-05 07:28:37 -05:00
Dave Horton
9ac144ef71 0.0.129 2026-03-05 07:24:30 -05:00
Hoan Luu Huu
337114035d support toolhook (#125) 2026-03-05 07:24:04 -05:00
Dave Horton
253bd8a49c 0.0.128 2026-02-26 07:25:14 -05:00
Hoan Luu Huu
7a5c094bfc houdifyOptions support audioQueryAbsoluteTimeout (#124) 2026-02-26 07:24:13 -05:00
Dave Horton
228f8773d3 0.0.127 2026-02-11 17:32:14 -05:00
Hoan Luu Huu
c9cd50c559 support noise isolation to config verb (#122)
* support noise isolation to config verb

* wip

* wip

* wip

* wip

* add vendor to turnTaking
2026-02-11 17:31:19 -05:00
Dave Horton
fe095be5c8 0.0.126 2026-02-01 13:43:23 -05:00
Dave Horton
71caf6bb53 allow listen to be nested in a conference verb (#123) 2026-02-01 13:42:49 -05:00
Dave Horton
887320fd5d 0.0.125 2026-01-21 07:28:59 -05:00
Hoan Luu Huu
f0ffdee9c6 support speechamtics end_of_utterance_silence_trigger (#121) 2026-01-21 07:28:44 -05:00
Dave Horton
1013db46d3 0.0.124 2026-01-21 07:23:25 -05:00
Sam Machin
8771e3f22f add statusHook to redirect (#120) 2026-01-21 07:22:54 -05:00
Dave Horton
ff757d3177 0.0.123 2026-01-02 10:20:58 -05:00
Sam Machin
5745cc9a29 add config:record type (#118) 2026-01-02 10:19:43 -05:00
5 changed files with 287 additions and 43 deletions

View File

@@ -4,6 +4,27 @@ const _specData = require('./specs');
const specs = new Map();
for (const key in _specData) { specs.set(key, _specData[key]); }
/* verb synonyms and shortcuts: maps alias verb names to their canonical form,
optionally injecting properties (e.g. vendor) into the verb data */
const verbTransforms = new Map([
['stream', {verb: 'listen'}],
['s2s', {verb: 'llm'}],
['openai_s2s', {verb: 'llm', properties: {vendor: 'openai'}}],
['microsoft_s2s', {verb: 'llm', properties: {vendor: 'microsoft'}}],
['google_s2s', {verb: 'llm', properties: {vendor: 'google'}}],
['elevenlabs_s2s', {verb: 'llm', properties: {vendor: 'elevenlabs'}}],
['deepgram_s2s', {verb: 'llm', properties: {vendor: 'deepgram'}}],
['voiceagent_s2s', {verb: 'llm', properties: {vendor: 'voiceagent'}}],
['ultravox_s2s', {verb: 'llm', properties: {vendor: 'ultravox'}}],
]);
function applyVerbTransform(name, data) {
const transform = verbTransforms.get(name);
if (!transform) return {name, data};
const newData = transform.properties ? {...transform.properties, ...data} : data;
return {name: transform.verb, data: newData};
}
function normalizeJambones(logger, obj) {
if (!Array.isArray(obj)) {
throw new Error('malformed jambonz payload: must be array');
@@ -13,18 +34,22 @@ function normalizeJambones(logger, obj) {
if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects');
if ('verb' in tdata) {
// {verb: 'say', text: 'foo..bar'..}
const name = tdata.verb;
const o = {};
Object.keys(tdata)
.filter((k) => k !== 'verb')
.forEach((k) => o[k] = tdata[k]);
const {name, data} = applyVerbTransform(tdata.verb, o);
const o2 = {};
o2[name] = o;
o2[name] = data;
document.push(o2);
}
else if (Object.keys(tdata).length === 1) {
// {'say': {..}}
document.push(tdata);
const key = Object.keys(tdata)[0];
const {name, data} = applyVerbTransform(key, tdata[key]);
const o2 = {};
o2[name] = data;
document.push(o2);
}
else {
logger.info(tdata, 'malformed jambonz payload: missing verb property');

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@jambonz/verb-specifications",
"version": "0.0.122",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@jambonz/verb-specifications",
"version": "0.0.122",
"version": "0.1.2",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",

View File

@@ -1,6 +1,6 @@
{
"name": "@jambonz/verb-specifications",
"version": "0.0.122",
"version": "0.1.2",
"description": "Jambonz Verb Specification Utilities",
"main": "index.js",
"scripts": {

View File

@@ -76,7 +76,9 @@
"referHook": "object|string",
"earlyMedia": "boolean",
"autoStreamTts": "boolean",
"disableTtsCache": "boolean"
"disableTtsCache": "boolean",
"noiseIsolation": "#noiseIsolation",
"turnTaking": "#turnTaking"
},
"required": []
},
@@ -280,6 +282,7 @@
"statusHook": "object|string",
"enterHook": "object|string",
"record": "#record",
"listen": "#listen",
"distributeDtmf": "boolean"
},
"required": [
@@ -329,7 +332,7 @@
"region": "string",
"model": {
"type": "string",
"enum": ["es", "cx"]
"enum": ["es", "cx", "ces"]
},
"lang": "string",
"actionHook": "object|string",
@@ -424,33 +427,6 @@
"url"
]
},
"stream": {
"properties": {
"id": "string",
"actionHook": "object|string",
"auth": "#auth",
"finishOnKey": "string",
"maxLength": "number",
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"playBeep": "boolean",
"disableBidirectionalAudio": "boolean",
"bidirectionalAudio": "#bidirectionalAudio",
"sampleRate": "number",
"timeout": "number",
"transcribe": "#transcribe",
"url": "string",
"wsAuth": "#auth",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"llm": {
"properties": {
"id": "string",
@@ -534,6 +510,11 @@
"type": "string",
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
},
"type" : {
"type" : "string",
"enum" : ["cloud", "siprec"]
},
"recordingID": "string",
"siprecServerURL": "string|array",
"headers": "object"
@@ -545,7 +526,8 @@
"redirect": {
"properties": {
"id": "string",
"actionHook": "object|string"
"actionHook": "object|string",
"statusHook": "object|string"
},
"required": [
"actionHook"
@@ -681,6 +663,7 @@
"fallbackLabel": "string",
"fallbackLanguage": "string",
"vad": "#vad",
"autogeneratePrompt": "boolean",
"hints": "array",
"hintsBoost": "number",
"altLanguages": "array",
@@ -874,7 +857,8 @@
"sessionTimeout": "number",
"connectionTimeout": "number",
"customVocabulary": "array",
"languageModel": "string"
"languageModel": "string",
"audioQueryAbsoluteTimeout": "number"
}
},
"elevenlabsOptions": {
@@ -1079,6 +1063,7 @@
"additional_vocab": "array",
"diarization": "string",
"speaker_diarization_config": "#sm_speakerDiarizationConfig",
"conversation_config": "#sm_conversationConfig",
"enable_partials": "boolean",
"max_delay": "number",
"max_delay_mode": {
@@ -1106,6 +1091,13 @@
"required": [
]
},
"sm_conversationConfig": {
"properties": {
"end_of_utterance_silence_trigger": "number"
},
"required": [
]
},
"sm_puctuationOverrides": {
"properties": {
"permitted_marks": "array",
@@ -1220,10 +1212,17 @@
"v3"
]
},
"speechModel": "string",
"formatTurns": "boolean",
"endOfTurnConfidenceThreshold": "number",
"minEndOfTurnSilenceWhenConfident": "number",
"maxTurnSilence": "number"
"maxTurnSilence": "number",
"minTurnSilence": "number",
"keyterms": "array",
"prompt": "string",
"languageDetection": "boolean",
"vadThreshold": "number",
"inactivityTimeout": "number"
}
},
"resource": {
@@ -1362,10 +1361,12 @@
"tts": "#synthesizer",
"vad": "#vad",
"turnDetection": "#turnDetectionPipeline",
"turnGate": "#turnGatePipeline",
"llm": "#llm",
"preflightLlm": "boolean",
"actionHook": "object|string",
"eventHook": "object|string"
"eventHook": "object|string",
"toolHook": "object|string"
},
"required": [
"stt",
@@ -1385,5 +1386,32 @@
"required": [
"vendor"
]
},
"turnGatePipeline": {
"properties": {
"enabled": "boolean",
"vendor": "string",
"model": "string",
"auth": "object"
},
"required": [
"enabled"
]
},
"noiseIsolation" : {
"properties": {
"enable": "boolean",
"vendor": "string",
"level": "number",
"model": "string"
}
},
"turnTaking": {
"properties": {
"enable": "boolean",
"vendor": "string",
"threshold": "number",
"model": "string"
}
}
}

View File

@@ -1,6 +1,6 @@
const test = require('tape');
const logger = require('pino')({level: process.env.JAMBONES_LOGLEVEL || 'error'});
const { validate } = require('..');
const { validate, normalizeJambones } = require('..');
test("validate correct verbs", async (t) => {
@@ -329,7 +329,8 @@ test("validate correct verbs", async (t) => {
"sessionTimeout": 30000,
"connectionTimeout": 5000,
"customVocabulary": ["jambonz", "telephony", "voip"],
"languageModel": "enhanced"
"languageModel": "enhanced",
"audioQueryAbsoluteTimeout": 5
},
"gladiaOptions": {
"post_processing": {
@@ -468,6 +469,24 @@ test("validate correct verbs", async (t) => {
"speechPadMs": 1000
}
},
{
"verb": "config",
"noiseIsolation": {
"enable": true,
"vendor": "krisp",
"level": 3,
"model": "custom-model"
}
},
{
"verb": "config",
"turnTaking": {
"enable": true,
"vendor": "krisp",
"threshold": 0.5,
"model": "turn-taking-model"
}
},
{
"verb": "message",
"to": "15083084809",
@@ -644,6 +663,74 @@ test("validate correct verbs", async (t) => {
}
}
]
},
{
"verb": "s2s",
"vendor": "openai",
"llmOptions": {
"model": "gpt-4o-realtime"
}
},
{
"verb": "openai_s2s",
"llmOptions": {
"model": "gpt-4o-realtime"
}
},
{
"verb": "google_s2s",
"llmOptions": {
"model": "gemini-2.0-flash"
}
},
{
"verb": "elevenlabs_s2s",
"llmOptions": {
"agentId": "agent-123"
}
},
{
"verb": "stream",
"url": "wss://myrecorder.example.com/calls",
"mixType": "stereo"
},
{
"verb": "pipeline",
"stt": {
"vendor": "google",
"language": "en-US"
},
"tts": {
"vendor": "google",
"language": "en-US"
},
"llm": {
"vendor": "openai",
"llmOptions": {
"model": "gpt-4o"
}
},
"actionHook": "/pipeline/action",
"eventHook": "/pipeline/event",
"toolHook": "/pipeline/tool"
},
{
"verb": "transcribe",
"transcriptionHook": "http://example.com/transcribe",
"recognizer": {
"vendor": "speechmatics",
"language": "en",
"speechmaticsOptions": {
"transcription_config": {
"language": "en",
"enable_partials": true,
"max_delay": 2,
"conversation_config": {
"end_of_utterance_silence_trigger": 0.5
}
}
}
}
}
];
try {
@@ -671,6 +758,110 @@ test('invalid test', async (t) => {
} catch(err) {
t.ok(1 == 1,'successfully validate verbs');
}
t.end();
})
});
test('verb synonyms: stream is synonym for listen', async (t) => {
// "verb" format
const result1 = normalizeJambones(logger, [
{"verb": "stream", "url": "wss://example.com/calls", "mixType": "stereo"}
]);
t.equal(Object.keys(result1[0])[0], 'listen', 'stream verb is rewritten to listen');
t.equal(result1[0].listen.url, 'wss://example.com/calls', 'data is preserved');
// object-key format
const result2 = normalizeJambones(logger, [
{"stream": {"url": "wss://example.com/calls"}}
]);
t.equal(Object.keys(result2[0])[0], 'listen', 'stream key is rewritten to listen');
// validate passes
try {
validate(logger, [{"verb": "stream", "url": "wss://example.com/calls"}]);
t.pass('stream verb validates successfully');
} catch (err) {
t.fail('stream verb should validate: ' + err);
}
t.end();
});
test('verb synonyms: s2s is synonym for llm', async (t) => {
const result = normalizeJambones(logger, [
{"verb": "s2s", "vendor": "openai", "llmOptions": {"model": "gpt-4o"}}
]);
t.equal(Object.keys(result[0])[0], 'llm', 's2s verb is rewritten to llm');
t.equal(result[0].llm.vendor, 'openai', 'vendor is preserved');
t.equal(result[0].llm.llmOptions.model, 'gpt-4o', 'llmOptions preserved');
try {
validate(logger, [{"verb": "s2s", "vendor": "openai", "llmOptions": {"model": "gpt-4o"}}]);
t.pass('s2s verb validates successfully');
} catch (err) {
t.fail('s2s verb should validate: ' + err);
}
t.end();
});
test('vendor shortcuts: openai_s2s injects vendor', async (t) => {
const result = normalizeJambones(logger, [
{"verb": "openai_s2s", "llmOptions": {"model": "gpt-4o-realtime"}}
]);
t.equal(Object.keys(result[0])[0], 'llm', 'openai_s2s is rewritten to llm');
t.equal(result[0].llm.vendor, 'openai', 'vendor is injected');
t.equal(result[0].llm.llmOptions.model, 'gpt-4o-realtime', 'llmOptions preserved');
try {
validate(logger, [{"verb": "openai_s2s", "llmOptions": {"model": "gpt-4o-realtime"}}]);
t.pass('openai_s2s validates successfully');
} catch (err) {
t.fail('openai_s2s should validate: ' + err);
}
t.end();
});
test('vendor shortcuts: all vendors work', async (t) => {
const vendors = [
'openai', 'microsoft', 'google', 'elevenlabs', 'deepgram', 'voiceagent', 'ultravox'
];
for (const vendor of vendors) {
const verbName = `${vendor}_s2s`;
const result = normalizeJambones(logger, [
{"verb": verbName, "llmOptions": {}}
]);
t.equal(Object.keys(result[0])[0], 'llm', `${verbName} rewrites to llm`);
t.equal(result[0].llm.vendor, vendor, `${verbName} injects vendor=${vendor}`);
}
t.end();
});
test('vendor shortcuts: object-key format works', async (t) => {
const result = normalizeJambones(logger, [
{"google_s2s": {"llmOptions": {"model": "gemini-2.0-flash"}}}
]);
t.equal(Object.keys(result[0])[0], 'llm', 'google_s2s key is rewritten to llm');
t.equal(result[0].llm.vendor, 'google', 'vendor is injected');
t.equal(result[0].llm.llmOptions.model, 'gemini-2.0-flash', 'llmOptions preserved');
t.end();
});
test('vendor shortcuts: explicit vendor in data overrides injected vendor', async (t) => {
const result = normalizeJambones(logger, [
{"verb": "openai_s2s", "vendor": "custom", "llmOptions": {}}
]);
t.equal(result[0].llm.vendor, 'custom', 'explicit vendor takes precedence');
t.end();
});
test('non-synonym verbs are unchanged', async (t) => {
const result = normalizeJambones(logger, [
{"verb": "say", "text": "hello"}
]);
t.equal(Object.keys(result[0])[0], 'say', 'say verb is not transformed');
const result2 = normalizeJambones(logger, [
{"llm": {"vendor": "openai", "llmOptions": {}}}
]);
t.equal(Object.keys(result2[0])[0], 'llm', 'llm verb is not transformed');
t.end();
});