@navai/voice-frontend 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +65 -24
- package/README.es.md +65 -24
- package/README.md +82 -28
- package/bin/generate-web-module-loaders.mjs +31 -3
- package/dist/index.cjs +437 -68
- package/dist/index.d.cts +45 -22
- package/dist/index.d.ts +45 -22
- package/dist/index.js +437 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -247,27 +247,33 @@ function getNavaiRoutePromptLines(routes = []) {
|
|
|
247
247
|
// src/agent.ts
|
|
248
248
|
var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set(["navigate_to", "execute_app_function"]);
|
|
249
249
|
var TOOL_NAME_REGEXP = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
250
|
+
var DEBUG_PREFIX = "[navai debug]";
|
|
250
251
|
function toErrorMessage2(error) {
|
|
251
252
|
return error instanceof Error ? error.message : String(error);
|
|
252
253
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
function debugLog(message, details) {
|
|
255
|
+
if (details === void 0) {
|
|
256
|
+
console.log(`${DEBUG_PREFIX} ${message}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(`${DEBUG_PREFIX} ${message}`, details);
|
|
260
|
+
}
|
|
261
|
+
function normalizeBackendFunctions(backendFunctions, functionsRegistry, warnings) {
|
|
256
262
|
const backendFunctionsByName = /* @__PURE__ */ new Map();
|
|
257
263
|
const backendFunctionsOrdered = [];
|
|
258
|
-
for (const backendFunction of
|
|
264
|
+
for (const backendFunction of backendFunctions ?? []) {
|
|
259
265
|
const name = backendFunction.name.trim().toLowerCase();
|
|
260
266
|
if (!name) {
|
|
261
267
|
continue;
|
|
262
268
|
}
|
|
263
269
|
if (functionsRegistry.byName.has(name)) {
|
|
264
|
-
|
|
270
|
+
warnings.push(
|
|
265
271
|
`[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
|
|
266
272
|
);
|
|
267
273
|
continue;
|
|
268
274
|
}
|
|
269
275
|
if (backendFunctionsByName.has(name)) {
|
|
270
|
-
|
|
276
|
+
warnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
|
|
271
277
|
continue;
|
|
272
278
|
}
|
|
273
279
|
const normalizedDefinition = {
|
|
@@ -277,94 +283,119 @@ async function buildNavaiAgent(options) {
|
|
|
277
283
|
backendFunctionsByName.set(name, normalizedDefinition);
|
|
278
284
|
backendFunctionsOrdered.push(normalizedDefinition);
|
|
279
285
|
}
|
|
286
|
+
return backendFunctionsOrdered;
|
|
287
|
+
}
|
|
288
|
+
function createExecuteAppFunction(input) {
|
|
289
|
+
const backendFunctionsByName = new Map(input.backendFunctions.map((item) => [item.name, item]));
|
|
280
290
|
const availableFunctionNames = [
|
|
281
|
-
...functionsRegistry.ordered.map((item) => item.name),
|
|
282
|
-
...
|
|
291
|
+
...input.functionsRegistry.ordered.map((item) => item.name),
|
|
292
|
+
...input.backendFunctions.map((item) => item.name)
|
|
283
293
|
];
|
|
284
|
-
const aliasWarnings = [];
|
|
285
|
-
const directFunctionToolNames = [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
|
|
286
|
-
if (!name) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
if (RESERVED_TOOL_NAMES.has(name)) {
|
|
290
|
-
aliasWarnings.push(
|
|
291
|
-
`[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
|
|
292
|
-
);
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
if (!TOOL_NAME_REGEXP.test(name)) {
|
|
296
|
-
aliasWarnings.push(
|
|
297
|
-
`[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
|
|
298
|
-
);
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
return true;
|
|
302
|
-
});
|
|
303
294
|
const executeAppFunction = async (requestedName, payload) => {
|
|
304
295
|
const requested = requestedName.trim().toLowerCase();
|
|
305
|
-
|
|
296
|
+
debugLog("execute_app_function called", { requestedName, requested, payload });
|
|
297
|
+
const frontendDefinition = input.functionsRegistry.byName.get(requested);
|
|
306
298
|
if (frontendDefinition) {
|
|
307
299
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
300
|
+
debugLog("executing frontend function", {
|
|
301
|
+
functionName: frontendDefinition.name,
|
|
302
|
+
source: frontendDefinition.source
|
|
303
|
+
});
|
|
304
|
+
const result = await frontendDefinition.run(payload ?? {}, input.context);
|
|
305
|
+
const response = {
|
|
306
|
+
ok: true,
|
|
307
|
+
function_name: frontendDefinition.name,
|
|
308
|
+
source: frontendDefinition.source,
|
|
309
|
+
result
|
|
310
|
+
};
|
|
311
|
+
debugLog("frontend function completed", response);
|
|
312
|
+
return response;
|
|
310
313
|
} catch (error) {
|
|
311
|
-
|
|
314
|
+
const failure = {
|
|
312
315
|
ok: false,
|
|
313
316
|
function_name: frontendDefinition.name,
|
|
314
317
|
error: "Function execution failed.",
|
|
315
318
|
details: toErrorMessage2(error)
|
|
316
319
|
};
|
|
320
|
+
debugLog("frontend function failed", failure);
|
|
321
|
+
return failure;
|
|
317
322
|
}
|
|
318
323
|
}
|
|
319
324
|
const backendDefinition = backendFunctionsByName.get(requested);
|
|
320
325
|
if (!backendDefinition) {
|
|
321
|
-
|
|
326
|
+
const failure = {
|
|
322
327
|
ok: false,
|
|
323
328
|
error: "Unknown or disallowed function.",
|
|
324
329
|
available_functions: availableFunctionNames
|
|
325
330
|
};
|
|
331
|
+
debugLog("execute_app_function rejected unknown function", failure);
|
|
332
|
+
return failure;
|
|
326
333
|
}
|
|
327
|
-
if (!
|
|
328
|
-
|
|
334
|
+
if (!input.executeBackendFunction) {
|
|
335
|
+
const failure = {
|
|
329
336
|
ok: false,
|
|
330
337
|
function_name: backendDefinition.name,
|
|
331
338
|
error: "Backend function execution is not configured."
|
|
332
339
|
};
|
|
340
|
+
debugLog("backend function execution unavailable", failure);
|
|
341
|
+
return failure;
|
|
333
342
|
}
|
|
334
343
|
try {
|
|
335
|
-
|
|
344
|
+
debugLog("executing backend function", {
|
|
345
|
+
functionName: backendDefinition.name,
|
|
346
|
+
source: backendDefinition.source ?? "backend"
|
|
347
|
+
});
|
|
348
|
+
const result = await input.executeBackendFunction({
|
|
336
349
|
functionName: backendDefinition.name,
|
|
337
350
|
payload: payload ?? null
|
|
338
351
|
});
|
|
339
|
-
|
|
352
|
+
const response = {
|
|
340
353
|
ok: true,
|
|
341
354
|
function_name: backendDefinition.name,
|
|
342
355
|
source: backendDefinition.source ?? "backend",
|
|
343
356
|
result
|
|
344
357
|
};
|
|
358
|
+
debugLog("backend function completed", response);
|
|
359
|
+
return response;
|
|
345
360
|
} catch (error) {
|
|
346
|
-
|
|
361
|
+
const failure = {
|
|
347
362
|
ok: false,
|
|
348
363
|
function_name: backendDefinition.name,
|
|
349
364
|
error: "Function execution failed.",
|
|
350
365
|
details: toErrorMessage2(error)
|
|
351
366
|
};
|
|
367
|
+
debugLog("backend function failed", failure);
|
|
368
|
+
return failure;
|
|
352
369
|
}
|
|
353
370
|
};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
371
|
+
return {
|
|
372
|
+
availableFunctionNames,
|
|
373
|
+
executeAppFunction
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function createFunctionTools(input) {
|
|
377
|
+
const aliasWarnings = [];
|
|
378
|
+
const availableFunctionNames = [
|
|
379
|
+
...input.functionsRegistry.ordered.map((item) => item.name),
|
|
380
|
+
...input.backendFunctions.map((item) => item.name)
|
|
381
|
+
];
|
|
382
|
+
const directFunctionToolNames = input.includeDirectAliases === false ? [] : [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
|
|
383
|
+
if (!name) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
if (RESERVED_TOOL_NAMES.has(name)) {
|
|
387
|
+
aliasWarnings.push(
|
|
388
|
+
`[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
|
|
389
|
+
);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (!TOOL_NAME_REGEXP.test(name)) {
|
|
393
|
+
aliasWarnings.push(
|
|
394
|
+
`[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
|
|
395
|
+
);
|
|
396
|
+
return false;
|
|
367
397
|
}
|
|
398
|
+
return true;
|
|
368
399
|
});
|
|
369
400
|
const executeFunctionTool = tool({
|
|
370
401
|
name: "execute_app_function",
|
|
@@ -375,36 +406,146 @@ async function buildNavaiAgent(options) {
|
|
|
375
406
|
"Payload object. Use null when no arguments are needed. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
|
|
376
407
|
)
|
|
377
408
|
}),
|
|
378
|
-
execute: async ({ function_name, payload }) => await executeAppFunction(function_name, payload)
|
|
409
|
+
execute: async ({ function_name, payload }) => await input.executeAppFunction(function_name, payload)
|
|
379
410
|
});
|
|
380
411
|
const directFunctionTools = directFunctionToolNames.map(
|
|
381
412
|
(functionName) => tool({
|
|
382
413
|
name: functionName,
|
|
383
414
|
description: `Direct alias for execute_app_function("${functionName}").`,
|
|
384
415
|
parameters: z.object({
|
|
385
|
-
payload: z.record(z.string(), z.unknown()).nullable().
|
|
416
|
+
payload: z.record(z.string(), z.unknown()).nullable().describe(
|
|
386
417
|
"Payload object. Optional. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
|
|
387
418
|
)
|
|
388
419
|
}),
|
|
389
|
-
execute: async ({ payload }) => await executeAppFunction(functionName, payload ?? null)
|
|
420
|
+
execute: async ({ payload }) => await input.executeAppFunction(functionName, payload ?? null)
|
|
390
421
|
})
|
|
391
422
|
);
|
|
392
|
-
|
|
393
|
-
|
|
423
|
+
return {
|
|
424
|
+
aliasWarnings,
|
|
425
|
+
availableFunctionNames,
|
|
426
|
+
executeFunctionTool,
|
|
427
|
+
directFunctionTools
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function buildFunctionLines(functionsRegistry, backendFunctions) {
|
|
431
|
+
return functionsRegistry.ordered.length + backendFunctions.length > 0 ? [
|
|
394
432
|
...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
|
|
395
|
-
...
|
|
396
|
-
(item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
|
|
397
|
-
)
|
|
433
|
+
...backendFunctions.map((item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`)
|
|
398
434
|
] : ["- none"];
|
|
435
|
+
}
|
|
436
|
+
async function buildNavaiAgent(options) {
|
|
437
|
+
const aggregatedWarnings = [];
|
|
438
|
+
const configuredAgents = (options.agents ?? []).filter(
|
|
439
|
+
(agent2) => Object.keys(agent2.functionModuleLoaders ?? {}).length > 0
|
|
440
|
+
);
|
|
441
|
+
const primaryAgentConfig = configuredAgents.find((agent2) => agent2.key === options.primaryAgentKey) ?? configuredAgents.find((agent2) => agent2.isPrimary) ?? configuredAgents[0];
|
|
442
|
+
const primaryFunctionLoaders = primaryAgentConfig?.functionModuleLoaders ?? options.functionModuleLoaders ?? {};
|
|
443
|
+
const functionsRegistry = await loadNavaiFunctions(primaryFunctionLoaders);
|
|
444
|
+
const backendFunctionsOrdered = normalizeBackendFunctions(options.backendFunctions, functionsRegistry, aggregatedWarnings);
|
|
445
|
+
const primaryExecutionSurface = createExecuteAppFunction({
|
|
446
|
+
functionsRegistry,
|
|
447
|
+
backendFunctions: backendFunctionsOrdered,
|
|
448
|
+
executeBackendFunction: options.executeBackendFunction,
|
|
449
|
+
context: options
|
|
450
|
+
});
|
|
451
|
+
const primaryFunctionTools = createFunctionTools({
|
|
452
|
+
functionsRegistry,
|
|
453
|
+
backendFunctions: backendFunctionsOrdered,
|
|
454
|
+
executeAppFunction: primaryExecutionSurface.executeAppFunction
|
|
455
|
+
});
|
|
456
|
+
aggregatedWarnings.push(...functionsRegistry.warnings, ...primaryFunctionTools.aliasWarnings);
|
|
457
|
+
const navigateTool = tool({
|
|
458
|
+
name: "navigate_to",
|
|
459
|
+
description: "Navigate to an allowed route in the current app.",
|
|
460
|
+
parameters: z.object({
|
|
461
|
+
target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
|
|
462
|
+
}),
|
|
463
|
+
execute: async ({ target }) => {
|
|
464
|
+
debugLog("navigate_to called", { target });
|
|
465
|
+
const path = resolveNavaiRoute(target, options.routes);
|
|
466
|
+
if (!path) {
|
|
467
|
+
const failure = { ok: false, error: "Unknown or disallowed route." };
|
|
468
|
+
debugLog("navigate_to rejected", failure);
|
|
469
|
+
return failure;
|
|
470
|
+
}
|
|
471
|
+
options.navigate(path);
|
|
472
|
+
const response = { ok: true, path };
|
|
473
|
+
debugLog("navigate_to completed", response);
|
|
474
|
+
return response;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
const routeLines = getNavaiRoutePromptLines(options.routes);
|
|
478
|
+
const functionLines = buildFunctionLines(functionsRegistry, backendFunctionsOrdered);
|
|
479
|
+
const specialistAgents = [];
|
|
480
|
+
const specialistLines = [];
|
|
481
|
+
for (const runtimeAgent of configuredAgents) {
|
|
482
|
+
if (primaryAgentConfig && runtimeAgent.key === primaryAgentConfig.key) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const specialistRegistry = await loadNavaiFunctions(runtimeAgent.functionModuleLoaders);
|
|
486
|
+
const specialistWarnings = [...specialistRegistry.warnings];
|
|
487
|
+
const specialistBackendFunctions = normalizeBackendFunctions(
|
|
488
|
+
options.backendFunctions,
|
|
489
|
+
specialistRegistry,
|
|
490
|
+
specialistWarnings
|
|
491
|
+
);
|
|
492
|
+
const specialistExecutionSurface = createExecuteAppFunction({
|
|
493
|
+
functionsRegistry: specialistRegistry,
|
|
494
|
+
backendFunctions: specialistBackendFunctions,
|
|
495
|
+
executeBackendFunction: options.executeBackendFunction,
|
|
496
|
+
context: options
|
|
497
|
+
});
|
|
498
|
+
const specialistFunctionTools = createFunctionTools({
|
|
499
|
+
functionsRegistry: specialistRegistry,
|
|
500
|
+
backendFunctions: specialistBackendFunctions,
|
|
501
|
+
executeAppFunction: specialistExecutionSurface.executeAppFunction,
|
|
502
|
+
includeDirectAliases: false
|
|
503
|
+
});
|
|
504
|
+
specialistWarnings.push(...specialistFunctionTools.aliasWarnings);
|
|
505
|
+
aggregatedWarnings.push(...specialistWarnings);
|
|
506
|
+
const specialistInstructions = [
|
|
507
|
+
runtimeAgent.instructions ?? `You are the ${runtimeAgent.name} specialist agent for this web app.`,
|
|
508
|
+
"Allowed app functions:",
|
|
509
|
+
...buildFunctionLines(specialistRegistry, specialistBackendFunctions),
|
|
510
|
+
"Rules:",
|
|
511
|
+
"- Always use execute_app_function for app actions.",
|
|
512
|
+
"- When no arguments are needed, call execute_app_function with payload set to null.",
|
|
513
|
+
"- Use only the functions available to this specialist agent.",
|
|
514
|
+
"- Do not navigate unless one of your allowed functions explicitly does so.",
|
|
515
|
+
"- Return a concise result to the main NAVAI agent."
|
|
516
|
+
].join("\n");
|
|
517
|
+
debugLog("creating specialist agent", {
|
|
518
|
+
key: runtimeAgent.key,
|
|
519
|
+
name: runtimeAgent.name,
|
|
520
|
+
functions: [
|
|
521
|
+
...specialistRegistry.ordered.map((item) => item.name),
|
|
522
|
+
...specialistBackendFunctions.map((item) => item.name)
|
|
523
|
+
]
|
|
524
|
+
});
|
|
525
|
+
const specialistAgent = new RealtimeAgent({
|
|
526
|
+
name: runtimeAgent.name,
|
|
527
|
+
handoffDescription: runtimeAgent.handoffDescription ?? runtimeAgent.description ?? `Delegate specialist work to ${runtimeAgent.name}.`,
|
|
528
|
+
instructions: specialistInstructions,
|
|
529
|
+
tools: [specialistFunctionTools.executeFunctionTool]
|
|
530
|
+
});
|
|
531
|
+
specialistAgents.push(specialistAgent);
|
|
532
|
+
specialistLines.push(
|
|
533
|
+
`- ${runtimeAgent.name}: ${runtimeAgent.description ?? runtimeAgent.handoffDescription ?? "Specialist agent available by delegation."}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
399
536
|
const instructions = [
|
|
400
|
-
options.baseInstructions ?? "You are
|
|
537
|
+
primaryAgentConfig?.instructions ?? options.baseInstructions ?? "You are the main NAVAI voice agent embedded in a web app.",
|
|
401
538
|
"Allowed routes:",
|
|
402
539
|
...routeLines,
|
|
403
540
|
"Allowed app functions:",
|
|
404
541
|
...functionLines,
|
|
542
|
+
"Available specialist agents:",
|
|
543
|
+
...specialistLines.length > 0 ? specialistLines : ["- none"],
|
|
405
544
|
"Rules:",
|
|
406
545
|
"- If user asks to go/open a section, always call navigate_to.",
|
|
407
|
-
"- If user asks to run an internal action, call execute_app_function or the matching direct function tool.",
|
|
546
|
+
"- If user asks to run an internal action that belongs to you, call execute_app_function or the matching direct function tool.",
|
|
547
|
+
"- If the task clearly belongs to a specialist agent, hand off to that specialist agent.",
|
|
548
|
+
"- Food recommendations, fast food, hamburgers, pizza, tacos, snacks, and meal suggestions belong to the food specialist.",
|
|
408
549
|
"- Always include payload in execute_app_function. Use null when no arguments are needed.",
|
|
409
550
|
"- For execute_app_function, pass arguments using payload.args (array).",
|
|
410
551
|
"- For class methods, pass payload.constructorArgs and payload.methodArgs.",
|
|
@@ -412,11 +553,23 @@ async function buildNavaiAgent(options) {
|
|
|
412
553
|
"- If destination/action is unclear, ask a brief clarifying question."
|
|
413
554
|
].join("\n");
|
|
414
555
|
const agent = new RealtimeAgent({
|
|
415
|
-
name: options.agentName ?? "Navai Voice Agent",
|
|
556
|
+
name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
|
|
416
557
|
instructions,
|
|
417
|
-
|
|
558
|
+
handoffs: specialistAgents,
|
|
559
|
+
tools: [
|
|
560
|
+
navigateTool,
|
|
561
|
+
primaryFunctionTools.executeFunctionTool,
|
|
562
|
+
...primaryFunctionTools.directFunctionTools
|
|
563
|
+
]
|
|
564
|
+
});
|
|
565
|
+
debugLog("created primary agent", {
|
|
566
|
+
name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
|
|
567
|
+
primaryAgentKey: primaryAgentConfig?.key ?? null,
|
|
568
|
+
directFunctions: functionsRegistry.ordered.map((item) => item.name),
|
|
569
|
+
backendFunctions: backendFunctionsOrdered.map((item) => item.name),
|
|
570
|
+
specialistDelegates: configuredAgents.filter((runtimeAgent) => runtimeAgent.key !== primaryAgentConfig?.key).map((runtimeAgent) => runtimeAgent.key)
|
|
418
571
|
});
|
|
419
|
-
return { agent, warnings:
|
|
572
|
+
return { agent, warnings: aggregatedWarnings };
|
|
420
573
|
}
|
|
421
574
|
|
|
422
575
|
// src/backend.ts
|
|
@@ -539,6 +692,7 @@ function createNavaiBackendClient(options = {}) {
|
|
|
539
692
|
// src/runtime.ts
|
|
540
693
|
var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
|
|
541
694
|
var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
|
|
695
|
+
var AGENTS_ENV_KEYS = ["NAVAI_AGENTS_FOLDERS"];
|
|
542
696
|
var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
|
|
543
697
|
async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
544
698
|
const warnings = [];
|
|
@@ -548,6 +702,7 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
|
548
702
|
const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
|
|
549
703
|
const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
|
|
550
704
|
const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
|
|
705
|
+
const agentsFolders = readOptional2(options.agentsFolders) ?? readFirstOptionalEnv(options.env, AGENTS_ENV_KEYS);
|
|
551
706
|
const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
|
|
552
707
|
const routes = await resolveRoutes({
|
|
553
708
|
routesFile,
|
|
@@ -559,12 +714,23 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
|
559
714
|
const functionModuleLoaders = resolveFunctionModuleLoaders({
|
|
560
715
|
indexedLoaders,
|
|
561
716
|
functionsFolders,
|
|
717
|
+
agentsFolders,
|
|
562
718
|
defaultFunctionsFolder,
|
|
563
719
|
warnings
|
|
564
720
|
});
|
|
721
|
+
const agents = await resolveRuntimeAgents({
|
|
722
|
+
indexedLoaders,
|
|
723
|
+
functionModuleLoaders,
|
|
724
|
+
functionsFolders,
|
|
725
|
+
agentsFolders,
|
|
726
|
+
defaultFunctionsFolder
|
|
727
|
+
});
|
|
728
|
+
const primaryAgentKey = agents.find((agent) => agent.isPrimary)?.key;
|
|
565
729
|
return {
|
|
566
730
|
routes,
|
|
567
731
|
functionModuleLoaders,
|
|
732
|
+
agents,
|
|
733
|
+
primaryAgentKey,
|
|
568
734
|
modelOverride,
|
|
569
735
|
warnings
|
|
570
736
|
};
|
|
@@ -600,10 +766,11 @@ async function resolveRoutes(input) {
|
|
|
600
766
|
}
|
|
601
767
|
function resolveFunctionModuleLoaders(input) {
|
|
602
768
|
const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
|
|
769
|
+
const agentFolders = parseCsvList(input.agentsFolders);
|
|
603
770
|
const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
|
|
604
|
-
const matchers = tokens.map((token) => createPathMatcher(token));
|
|
771
|
+
const matchers = tokens.map((token) => createPathMatcher(token, agentFolders));
|
|
605
772
|
const matchedEntries = input.indexedLoaders.filter(
|
|
606
|
-
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
|
|
773
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && matchers.some((matcher) => matcher(entry.normalizedPath))
|
|
607
774
|
);
|
|
608
775
|
if (matchedEntries.length > 0) {
|
|
609
776
|
return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
@@ -613,9 +780,9 @@ function resolveFunctionModuleLoaders(input) {
|
|
|
613
780
|
`[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
|
|
614
781
|
);
|
|
615
782
|
}
|
|
616
|
-
const
|
|
783
|
+
const fallbackMatcherWithAgents = createPathMatcher(input.defaultFunctionsFolder, agentFolders);
|
|
617
784
|
const fallbackEntries = input.indexedLoaders.filter(
|
|
618
|
-
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") &&
|
|
785
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && fallbackMatcherWithAgents(entry.normalizedPath)
|
|
619
786
|
);
|
|
620
787
|
return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
621
788
|
}
|
|
@@ -658,7 +825,7 @@ function buildModuleCandidates(inputPath) {
|
|
|
658
825
|
}
|
|
659
826
|
return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
|
|
660
827
|
}
|
|
661
|
-
function createPathMatcher(input) {
|
|
828
|
+
function createPathMatcher(input, agentFolders = []) {
|
|
662
829
|
const raw = normalizePath(input);
|
|
663
830
|
if (!raw) {
|
|
664
831
|
return () => false;
|
|
@@ -676,8 +843,141 @@ function createPathMatcher(input) {
|
|
|
676
843
|
return (path) => path === normalized;
|
|
677
844
|
}
|
|
678
845
|
const base = normalized.replace(/\/+$/, "");
|
|
846
|
+
const normalizedAgents = agentFolders.map(normalizePathSegment).filter(Boolean);
|
|
847
|
+
if (normalizedAgents.length > 0) {
|
|
848
|
+
return (path) => {
|
|
849
|
+
if (!path.startsWith(`${base}/`)) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
const suffix = path.slice(base.length + 1);
|
|
853
|
+
const firstSegment = suffix.split("/", 1)[0] ?? "";
|
|
854
|
+
return normalizedAgents.includes(firstSegment);
|
|
855
|
+
};
|
|
856
|
+
}
|
|
679
857
|
return (path) => path === base || path.startsWith(`${base}/`);
|
|
680
858
|
}
|
|
859
|
+
async function resolveRuntimeAgents(input) {
|
|
860
|
+
const configuredAgents = parseCsvList(input.agentsFolders);
|
|
861
|
+
if (configuredAgents.length === 0) {
|
|
862
|
+
return [];
|
|
863
|
+
}
|
|
864
|
+
const loaderByPath = new Map(input.indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
|
|
865
|
+
const baseDirectories = resolveAgentBaseDirectories(input.functionsFolders, input.defaultFunctionsFolder);
|
|
866
|
+
const groupedLoaders = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const [rawPath, load] of Object.entries(input.functionModuleLoaders)) {
|
|
868
|
+
const agentKey = extractAgentKeyFromPath(rawPath, baseDirectories, configuredAgents);
|
|
869
|
+
if (!agentKey) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const current = groupedLoaders.get(agentKey) ?? {};
|
|
873
|
+
current[rawPath] = load;
|
|
874
|
+
groupedLoaders.set(agentKey, current);
|
|
875
|
+
}
|
|
876
|
+
const configuredPrimaryKey = configuredAgents[0];
|
|
877
|
+
const agents = [];
|
|
878
|
+
for (const agentKey of configuredAgents) {
|
|
879
|
+
const functionLoaders = groupedLoaders.get(agentKey);
|
|
880
|
+
if (!functionLoaders || Object.keys(functionLoaders).length === 0) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const config = await loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath);
|
|
884
|
+
agents.push({
|
|
885
|
+
key: config.key?.trim() || agentKey,
|
|
886
|
+
name: readOptional2(config.name) ?? humanizeAgentKey(agentKey),
|
|
887
|
+
description: readOptional2(config.description),
|
|
888
|
+
handoffDescription: readOptional2(config.handoffDescription) ?? readOptional2(config.description),
|
|
889
|
+
instructions: readOptional2(config.instructions),
|
|
890
|
+
isPrimary: config.isPrimary === true || agentKey === configuredPrimaryKey,
|
|
891
|
+
functionModuleLoaders: functionLoaders
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
if (agents.filter((agent) => agent.isPrimary).length === 0 && agents[0]) {
|
|
895
|
+
agents[0].isPrimary = true;
|
|
896
|
+
}
|
|
897
|
+
if (agents.filter((agent) => agent.isPrimary).length > 1) {
|
|
898
|
+
let primaryAssigned = false;
|
|
899
|
+
for (const agent of agents) {
|
|
900
|
+
if (agent.isPrimary && !primaryAssigned) {
|
|
901
|
+
primaryAssigned = true;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
agent.isPrimary = false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return agents;
|
|
908
|
+
}
|
|
909
|
+
async function loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath) {
|
|
910
|
+
for (const baseDirectory of baseDirectories) {
|
|
911
|
+
const configBase = `${baseDirectory}/${agentKey}/agent.config`;
|
|
912
|
+
const matchedLoader = buildModuleCandidates(configBase).map((candidate) => loaderByPath.get(candidate)).find(Boolean);
|
|
913
|
+
if (!matchedLoader) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const imported = await matchedLoader.load();
|
|
918
|
+
return readAgentModuleConfig(imported);
|
|
919
|
+
} catch {
|
|
920
|
+
return {};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return {};
|
|
924
|
+
}
|
|
925
|
+
function readAgentModuleConfig(moduleShape) {
|
|
926
|
+
const candidate = readRecord(moduleShape.NAVAI_AGENT) ?? readRecord(moduleShape.agent) ?? readRecord(moduleShape.default) ?? {};
|
|
927
|
+
return {
|
|
928
|
+
key: readOptionalString(candidate.key),
|
|
929
|
+
name: readOptionalString(candidate.name),
|
|
930
|
+
description: readOptionalString(candidate.description),
|
|
931
|
+
handoffDescription: readOptionalString(candidate.handoffDescription),
|
|
932
|
+
instructions: readOptionalString(candidate.instructions),
|
|
933
|
+
isPrimary: candidate.isPrimary === true
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function readRecord(value) {
|
|
937
|
+
return value && typeof value === "object" ? value : null;
|
|
938
|
+
}
|
|
939
|
+
function readOptionalString(value) {
|
|
940
|
+
return typeof value === "string" ? readOptional2(value) : void 0;
|
|
941
|
+
}
|
|
942
|
+
function resolveAgentBaseDirectories(functionsFolders, defaultFunctionsFolder) {
|
|
943
|
+
const configuredTokens = functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
|
|
944
|
+
const tokens = configuredTokens.length > 0 ? configuredTokens : [defaultFunctionsFolder];
|
|
945
|
+
return [...new Set(tokens.map(toAgentBaseDirectory).filter(Boolean))];
|
|
946
|
+
}
|
|
947
|
+
function toAgentBaseDirectory(input) {
|
|
948
|
+
const raw = normalizePath(input);
|
|
949
|
+
if (!raw) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
|
|
953
|
+
if (normalized.includes("*") || /\.[cm]?[jt]s$/.test(normalized)) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
if (normalized.endsWith("/...")) {
|
|
957
|
+
return normalized.slice(0, -4).replace(/\/+$/, "") || null;
|
|
958
|
+
}
|
|
959
|
+
return normalized.replace(/\/+$/, "") || null;
|
|
960
|
+
}
|
|
961
|
+
function extractAgentKeyFromPath(pathValue, baseDirectories, configuredAgents) {
|
|
962
|
+
const normalized = normalizePath(pathValue);
|
|
963
|
+
for (const baseDirectory of baseDirectories) {
|
|
964
|
+
if (!normalized.startsWith(`${baseDirectory}/`)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const suffix = normalized.slice(baseDirectory.length + 1);
|
|
968
|
+
const firstSegment = suffix.split("/", 1)[0] ?? "";
|
|
969
|
+
if (configuredAgents.includes(firstSegment)) {
|
|
970
|
+
return firstSegment;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return void 0;
|
|
974
|
+
}
|
|
975
|
+
function humanizeAgentKey(value) {
|
|
976
|
+
return value.split(/[_-]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
977
|
+
}
|
|
978
|
+
function isAgentConfigPath(pathValue) {
|
|
979
|
+
return /\/agent\.config\.[cm]?[jt]s$/i.test(pathValue);
|
|
980
|
+
}
|
|
681
981
|
function globToRegExp(pattern) {
|
|
682
982
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
683
983
|
const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
|
|
@@ -688,6 +988,12 @@ function globToRegExp(pattern) {
|
|
|
688
988
|
function normalizePath(input) {
|
|
689
989
|
return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
|
|
690
990
|
}
|
|
991
|
+
function normalizePathSegment(input) {
|
|
992
|
+
return normalizePath(input).replace(/\//g, "");
|
|
993
|
+
}
|
|
994
|
+
function parseCsvList(input) {
|
|
995
|
+
return (input ?? "").split(",").map((value) => normalizePathSegment(value)).filter(Boolean);
|
|
996
|
+
}
|
|
691
997
|
function readFirstOptionalEnv(env, keys) {
|
|
692
998
|
if (!env) {
|
|
693
999
|
return void 0;
|
|
@@ -718,6 +1024,7 @@ function toErrorMessage3(error) {
|
|
|
718
1024
|
// src/useWebVoiceAgent.ts
|
|
719
1025
|
import { RealtimeSession } from "@openai/agents/realtime";
|
|
720
1026
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1027
|
+
var DEBUG_PREFIX2 = "[navai debug]";
|
|
721
1028
|
function formatError(error) {
|
|
722
1029
|
if (error instanceof Error) {
|
|
723
1030
|
return error.message;
|
|
@@ -731,6 +1038,13 @@ function emitWarnings(warnings) {
|
|
|
731
1038
|
}
|
|
732
1039
|
}
|
|
733
1040
|
}
|
|
1041
|
+
function debugLog2(message, details) {
|
|
1042
|
+
if (details === void 0) {
|
|
1043
|
+
console.log(`${DEBUG_PREFIX2} ${message}`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
console.log(`${DEBUG_PREFIX2} ${message}`, details);
|
|
1047
|
+
}
|
|
734
1048
|
function useWebVoiceAgent(options) {
|
|
735
1049
|
const sessionRef = useRef(null);
|
|
736
1050
|
const attachedRealtimeSessionRef = useRef(null);
|
|
@@ -741,6 +1055,7 @@ function useWebVoiceAgent(options) {
|
|
|
741
1055
|
env: options.env,
|
|
742
1056
|
routesFile: options.routesFile,
|
|
743
1057
|
functionsFolders: options.functionsFolders,
|
|
1058
|
+
agentsFolders: options.agentsFolders,
|
|
744
1059
|
modelOverride: options.modelOverride,
|
|
745
1060
|
defaultRoutesFile: options.defaultRoutesFile,
|
|
746
1061
|
defaultFunctionsFolder: options.defaultFunctionsFolder
|
|
@@ -749,6 +1064,7 @@ function useWebVoiceAgent(options) {
|
|
|
749
1064
|
options.defaultFunctionsFolder,
|
|
750
1065
|
options.defaultRoutes,
|
|
751
1066
|
options.defaultRoutesFile,
|
|
1067
|
+
options.agentsFolders,
|
|
752
1068
|
options.env,
|
|
753
1069
|
options.functionsFolders,
|
|
754
1070
|
options.modelOverride,
|
|
@@ -795,6 +1111,45 @@ function useWebVoiceAgent(options) {
|
|
|
795
1111
|
const attachSessionAudioListeners = useCallback(
|
|
796
1112
|
(session) => {
|
|
797
1113
|
detachSessionAudioListeners();
|
|
1114
|
+
session.on("agent_start", (_context, agent, turnInput) => {
|
|
1115
|
+
debugLog2("session agent_start", {
|
|
1116
|
+
agent: agent.name,
|
|
1117
|
+
turnInputCount: Array.isArray(turnInput) ? turnInput.length : 0
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
session.on("agent_end", (_context, agent, output) => {
|
|
1121
|
+
debugLog2("session agent_end", {
|
|
1122
|
+
agent: agent.name,
|
|
1123
|
+
output
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
session.on("agent_handoff", (_context, fromAgent, toAgent) => {
|
|
1127
|
+
debugLog2("session agent_handoff", {
|
|
1128
|
+
from: fromAgent.name,
|
|
1129
|
+
to: toAgent.name
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
session.on("agent_tool_start", (_context, agent, tool2, details) => {
|
|
1133
|
+
debugLog2("session agent_tool_start", {
|
|
1134
|
+
agent: agent.name,
|
|
1135
|
+
tool: tool2.name,
|
|
1136
|
+
toolCall: details.toolCall
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
session.on("agent_tool_end", (_context, agent, tool2, result, details) => {
|
|
1140
|
+
debugLog2("session agent_tool_end", {
|
|
1141
|
+
agent: agent.name,
|
|
1142
|
+
tool: tool2.name,
|
|
1143
|
+
result,
|
|
1144
|
+
toolCall: details.toolCall
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
session.on("history_added", (item) => {
|
|
1148
|
+
debugLog2("session history_added", item);
|
|
1149
|
+
});
|
|
1150
|
+
session.on("error", (sessionError) => {
|
|
1151
|
+
debugLog2("session error", sessionError);
|
|
1152
|
+
});
|
|
798
1153
|
session.on("audio_start", handleSessionAudioStart);
|
|
799
1154
|
session.on("audio_stopped", handleSessionAudioStopped);
|
|
800
1155
|
session.on("audio_interrupted", handleSessionAudioInterrupted);
|
|
@@ -833,6 +1188,17 @@ function useWebVoiceAgent(options) {
|
|
|
833
1188
|
setAgentVoiceStateIfChanged("idle");
|
|
834
1189
|
try {
|
|
835
1190
|
const runtimeConfig = await runtimeConfigPromise;
|
|
1191
|
+
debugLog2("resolved runtime config", {
|
|
1192
|
+
routes: runtimeConfig.routes.map((route) => route.path),
|
|
1193
|
+
functionModules: Object.keys(runtimeConfig.functionModuleLoaders),
|
|
1194
|
+
agents: runtimeConfig.agents.map((agent2) => ({
|
|
1195
|
+
key: agent2.key,
|
|
1196
|
+
name: agent2.name,
|
|
1197
|
+
isPrimary: agent2.isPrimary,
|
|
1198
|
+
functionModules: Object.keys(agent2.functionModuleLoaders)
|
|
1199
|
+
})),
|
|
1200
|
+
warnings: runtimeConfig.warnings
|
|
1201
|
+
});
|
|
836
1202
|
const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
|
|
837
1203
|
const secretPayload = await backendClient.createClientSecret(requestPayload);
|
|
838
1204
|
const backendFunctionsResult = await backendClient.listFunctions();
|
|
@@ -840,6 +1206,8 @@ function useWebVoiceAgent(options) {
|
|
|
840
1206
|
navigate: options.navigate,
|
|
841
1207
|
routes: runtimeConfig.routes,
|
|
842
1208
|
functionModuleLoaders: runtimeConfig.functionModuleLoaders,
|
|
1209
|
+
agents: runtimeConfig.agents,
|
|
1210
|
+
primaryAgentKey: runtimeConfig.primaryAgentKey,
|
|
843
1211
|
backendFunctions: backendFunctionsResult.functions,
|
|
844
1212
|
executeBackendFunction: backendClient.executeFunction
|
|
845
1213
|
});
|
|
@@ -855,6 +1223,7 @@ function useWebVoiceAgent(options) {
|
|
|
855
1223
|
setStatus("connected");
|
|
856
1224
|
} catch (startError) {
|
|
857
1225
|
const message = formatError(startError);
|
|
1226
|
+
debugLog2("session start failed", { message, error: startError });
|
|
858
1227
|
setError(message);
|
|
859
1228
|
setStatus("error");
|
|
860
1229
|
setAgentVoiceStateIfChanged("idle");
|