@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.cjs
CHANGED
|
@@ -823,27 +823,33 @@ function getNavaiRoutePromptLines(routes = []) {
|
|
|
823
823
|
// src/agent.ts
|
|
824
824
|
var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set(["navigate_to", "execute_app_function"]);
|
|
825
825
|
var TOOL_NAME_REGEXP = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
826
|
+
var DEBUG_PREFIX = "[navai debug]";
|
|
826
827
|
function toErrorMessage2(error) {
|
|
827
828
|
return error instanceof Error ? error.message : String(error);
|
|
828
829
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
830
|
+
function debugLog(message, details) {
|
|
831
|
+
if (details === void 0) {
|
|
832
|
+
console.log(`${DEBUG_PREFIX} ${message}`);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
console.log(`${DEBUG_PREFIX} ${message}`, details);
|
|
836
|
+
}
|
|
837
|
+
function normalizeBackendFunctions(backendFunctions, functionsRegistry, warnings) {
|
|
832
838
|
const backendFunctionsByName = /* @__PURE__ */ new Map();
|
|
833
839
|
const backendFunctionsOrdered = [];
|
|
834
|
-
for (const backendFunction of
|
|
840
|
+
for (const backendFunction of backendFunctions ?? []) {
|
|
835
841
|
const name = backendFunction.name.trim().toLowerCase();
|
|
836
842
|
if (!name) {
|
|
837
843
|
continue;
|
|
838
844
|
}
|
|
839
845
|
if (functionsRegistry.byName.has(name)) {
|
|
840
|
-
|
|
846
|
+
warnings.push(
|
|
841
847
|
`[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
|
|
842
848
|
);
|
|
843
849
|
continue;
|
|
844
850
|
}
|
|
845
851
|
if (backendFunctionsByName.has(name)) {
|
|
846
|
-
|
|
852
|
+
warnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
|
|
847
853
|
continue;
|
|
848
854
|
}
|
|
849
855
|
const normalizedDefinition = {
|
|
@@ -853,94 +859,119 @@ async function buildNavaiAgent(options) {
|
|
|
853
859
|
backendFunctionsByName.set(name, normalizedDefinition);
|
|
854
860
|
backendFunctionsOrdered.push(normalizedDefinition);
|
|
855
861
|
}
|
|
862
|
+
return backendFunctionsOrdered;
|
|
863
|
+
}
|
|
864
|
+
function createExecuteAppFunction(input) {
|
|
865
|
+
const backendFunctionsByName = new Map(input.backendFunctions.map((item) => [item.name, item]));
|
|
856
866
|
const availableFunctionNames = [
|
|
857
|
-
...functionsRegistry.ordered.map((item) => item.name),
|
|
858
|
-
...
|
|
867
|
+
...input.functionsRegistry.ordered.map((item) => item.name),
|
|
868
|
+
...input.backendFunctions.map((item) => item.name)
|
|
859
869
|
];
|
|
860
|
-
const aliasWarnings = [];
|
|
861
|
-
const directFunctionToolNames = [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
|
|
862
|
-
if (!name) {
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
if (RESERVED_TOOL_NAMES.has(name)) {
|
|
866
|
-
aliasWarnings.push(
|
|
867
|
-
`[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
|
|
868
|
-
);
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
if (!TOOL_NAME_REGEXP.test(name)) {
|
|
872
|
-
aliasWarnings.push(
|
|
873
|
-
`[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
|
|
874
|
-
);
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
return true;
|
|
878
|
-
});
|
|
879
870
|
const executeAppFunction = async (requestedName, payload) => {
|
|
880
871
|
const requested = requestedName.trim().toLowerCase();
|
|
881
|
-
|
|
872
|
+
debugLog("execute_app_function called", { requestedName, requested, payload });
|
|
873
|
+
const frontendDefinition = input.functionsRegistry.byName.get(requested);
|
|
882
874
|
if (frontendDefinition) {
|
|
883
875
|
try {
|
|
884
|
-
|
|
885
|
-
|
|
876
|
+
debugLog("executing frontend function", {
|
|
877
|
+
functionName: frontendDefinition.name,
|
|
878
|
+
source: frontendDefinition.source
|
|
879
|
+
});
|
|
880
|
+
const result = await frontendDefinition.run(payload ?? {}, input.context);
|
|
881
|
+
const response = {
|
|
882
|
+
ok: true,
|
|
883
|
+
function_name: frontendDefinition.name,
|
|
884
|
+
source: frontendDefinition.source,
|
|
885
|
+
result
|
|
886
|
+
};
|
|
887
|
+
debugLog("frontend function completed", response);
|
|
888
|
+
return response;
|
|
886
889
|
} catch (error) {
|
|
887
|
-
|
|
890
|
+
const failure = {
|
|
888
891
|
ok: false,
|
|
889
892
|
function_name: frontendDefinition.name,
|
|
890
893
|
error: "Function execution failed.",
|
|
891
894
|
details: toErrorMessage2(error)
|
|
892
895
|
};
|
|
896
|
+
debugLog("frontend function failed", failure);
|
|
897
|
+
return failure;
|
|
893
898
|
}
|
|
894
899
|
}
|
|
895
900
|
const backendDefinition = backendFunctionsByName.get(requested);
|
|
896
901
|
if (!backendDefinition) {
|
|
897
|
-
|
|
902
|
+
const failure = {
|
|
898
903
|
ok: false,
|
|
899
904
|
error: "Unknown or disallowed function.",
|
|
900
905
|
available_functions: availableFunctionNames
|
|
901
906
|
};
|
|
907
|
+
debugLog("execute_app_function rejected unknown function", failure);
|
|
908
|
+
return failure;
|
|
902
909
|
}
|
|
903
|
-
if (!
|
|
904
|
-
|
|
910
|
+
if (!input.executeBackendFunction) {
|
|
911
|
+
const failure = {
|
|
905
912
|
ok: false,
|
|
906
913
|
function_name: backendDefinition.name,
|
|
907
914
|
error: "Backend function execution is not configured."
|
|
908
915
|
};
|
|
916
|
+
debugLog("backend function execution unavailable", failure);
|
|
917
|
+
return failure;
|
|
909
918
|
}
|
|
910
919
|
try {
|
|
911
|
-
|
|
920
|
+
debugLog("executing backend function", {
|
|
921
|
+
functionName: backendDefinition.name,
|
|
922
|
+
source: backendDefinition.source ?? "backend"
|
|
923
|
+
});
|
|
924
|
+
const result = await input.executeBackendFunction({
|
|
912
925
|
functionName: backendDefinition.name,
|
|
913
926
|
payload: payload ?? null
|
|
914
927
|
});
|
|
915
|
-
|
|
928
|
+
const response = {
|
|
916
929
|
ok: true,
|
|
917
930
|
function_name: backendDefinition.name,
|
|
918
931
|
source: backendDefinition.source ?? "backend",
|
|
919
932
|
result
|
|
920
933
|
};
|
|
934
|
+
debugLog("backend function completed", response);
|
|
935
|
+
return response;
|
|
921
936
|
} catch (error) {
|
|
922
|
-
|
|
937
|
+
const failure = {
|
|
923
938
|
ok: false,
|
|
924
939
|
function_name: backendDefinition.name,
|
|
925
940
|
error: "Function execution failed.",
|
|
926
941
|
details: toErrorMessage2(error)
|
|
927
942
|
};
|
|
943
|
+
debugLog("backend function failed", failure);
|
|
944
|
+
return failure;
|
|
928
945
|
}
|
|
929
946
|
};
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
947
|
+
return {
|
|
948
|
+
availableFunctionNames,
|
|
949
|
+
executeAppFunction
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function createFunctionTools(input) {
|
|
953
|
+
const aliasWarnings = [];
|
|
954
|
+
const availableFunctionNames = [
|
|
955
|
+
...input.functionsRegistry.ordered.map((item) => item.name),
|
|
956
|
+
...input.backendFunctions.map((item) => item.name)
|
|
957
|
+
];
|
|
958
|
+
const directFunctionToolNames = input.includeDirectAliases === false ? [] : [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
|
|
959
|
+
if (!name) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
if (RESERVED_TOOL_NAMES.has(name)) {
|
|
963
|
+
aliasWarnings.push(
|
|
964
|
+
`[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
|
|
965
|
+
);
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
if (!TOOL_NAME_REGEXP.test(name)) {
|
|
969
|
+
aliasWarnings.push(
|
|
970
|
+
`[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
|
|
971
|
+
);
|
|
972
|
+
return false;
|
|
943
973
|
}
|
|
974
|
+
return true;
|
|
944
975
|
});
|
|
945
976
|
const executeFunctionTool = (0, import_realtime.tool)({
|
|
946
977
|
name: "execute_app_function",
|
|
@@ -951,36 +982,146 @@ async function buildNavaiAgent(options) {
|
|
|
951
982
|
"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."
|
|
952
983
|
)
|
|
953
984
|
}),
|
|
954
|
-
execute: async ({ function_name, payload }) => await executeAppFunction(function_name, payload)
|
|
985
|
+
execute: async ({ function_name, payload }) => await input.executeAppFunction(function_name, payload)
|
|
955
986
|
});
|
|
956
987
|
const directFunctionTools = directFunctionToolNames.map(
|
|
957
988
|
(functionName) => (0, import_realtime.tool)({
|
|
958
989
|
name: functionName,
|
|
959
990
|
description: `Direct alias for execute_app_function("${functionName}").`,
|
|
960
991
|
parameters: import_zod.z.object({
|
|
961
|
-
payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).nullable().
|
|
992
|
+
payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).nullable().describe(
|
|
962
993
|
"Payload object. Optional. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
|
|
963
994
|
)
|
|
964
995
|
}),
|
|
965
|
-
execute: async ({ payload }) => await executeAppFunction(functionName, payload ?? null)
|
|
996
|
+
execute: async ({ payload }) => await input.executeAppFunction(functionName, payload ?? null)
|
|
966
997
|
})
|
|
967
998
|
);
|
|
968
|
-
|
|
969
|
-
|
|
999
|
+
return {
|
|
1000
|
+
aliasWarnings,
|
|
1001
|
+
availableFunctionNames,
|
|
1002
|
+
executeFunctionTool,
|
|
1003
|
+
directFunctionTools
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function buildFunctionLines(functionsRegistry, backendFunctions) {
|
|
1007
|
+
return functionsRegistry.ordered.length + backendFunctions.length > 0 ? [
|
|
970
1008
|
...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
|
|
971
|
-
...
|
|
972
|
-
(item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
|
|
973
|
-
)
|
|
1009
|
+
...backendFunctions.map((item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`)
|
|
974
1010
|
] : ["- none"];
|
|
1011
|
+
}
|
|
1012
|
+
async function buildNavaiAgent(options) {
|
|
1013
|
+
const aggregatedWarnings = [];
|
|
1014
|
+
const configuredAgents = (options.agents ?? []).filter(
|
|
1015
|
+
(agent2) => Object.keys(agent2.functionModuleLoaders ?? {}).length > 0
|
|
1016
|
+
);
|
|
1017
|
+
const primaryAgentConfig = configuredAgents.find((agent2) => agent2.key === options.primaryAgentKey) ?? configuredAgents.find((agent2) => agent2.isPrimary) ?? configuredAgents[0];
|
|
1018
|
+
const primaryFunctionLoaders = primaryAgentConfig?.functionModuleLoaders ?? options.functionModuleLoaders ?? {};
|
|
1019
|
+
const functionsRegistry = await loadNavaiFunctions(primaryFunctionLoaders);
|
|
1020
|
+
const backendFunctionsOrdered = normalizeBackendFunctions(options.backendFunctions, functionsRegistry, aggregatedWarnings);
|
|
1021
|
+
const primaryExecutionSurface = createExecuteAppFunction({
|
|
1022
|
+
functionsRegistry,
|
|
1023
|
+
backendFunctions: backendFunctionsOrdered,
|
|
1024
|
+
executeBackendFunction: options.executeBackendFunction,
|
|
1025
|
+
context: options
|
|
1026
|
+
});
|
|
1027
|
+
const primaryFunctionTools = createFunctionTools({
|
|
1028
|
+
functionsRegistry,
|
|
1029
|
+
backendFunctions: backendFunctionsOrdered,
|
|
1030
|
+
executeAppFunction: primaryExecutionSurface.executeAppFunction
|
|
1031
|
+
});
|
|
1032
|
+
aggregatedWarnings.push(...functionsRegistry.warnings, ...primaryFunctionTools.aliasWarnings);
|
|
1033
|
+
const navigateTool = (0, import_realtime.tool)({
|
|
1034
|
+
name: "navigate_to",
|
|
1035
|
+
description: "Navigate to an allowed route in the current app.",
|
|
1036
|
+
parameters: import_zod.z.object({
|
|
1037
|
+
target: import_zod.z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
|
|
1038
|
+
}),
|
|
1039
|
+
execute: async ({ target }) => {
|
|
1040
|
+
debugLog("navigate_to called", { target });
|
|
1041
|
+
const path = resolveNavaiRoute(target, options.routes);
|
|
1042
|
+
if (!path) {
|
|
1043
|
+
const failure = { ok: false, error: "Unknown or disallowed route." };
|
|
1044
|
+
debugLog("navigate_to rejected", failure);
|
|
1045
|
+
return failure;
|
|
1046
|
+
}
|
|
1047
|
+
options.navigate(path);
|
|
1048
|
+
const response = { ok: true, path };
|
|
1049
|
+
debugLog("navigate_to completed", response);
|
|
1050
|
+
return response;
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
const routeLines = getNavaiRoutePromptLines(options.routes);
|
|
1054
|
+
const functionLines = buildFunctionLines(functionsRegistry, backendFunctionsOrdered);
|
|
1055
|
+
const specialistAgents = [];
|
|
1056
|
+
const specialistLines = [];
|
|
1057
|
+
for (const runtimeAgent of configuredAgents) {
|
|
1058
|
+
if (primaryAgentConfig && runtimeAgent.key === primaryAgentConfig.key) {
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const specialistRegistry = await loadNavaiFunctions(runtimeAgent.functionModuleLoaders);
|
|
1062
|
+
const specialistWarnings = [...specialistRegistry.warnings];
|
|
1063
|
+
const specialistBackendFunctions = normalizeBackendFunctions(
|
|
1064
|
+
options.backendFunctions,
|
|
1065
|
+
specialistRegistry,
|
|
1066
|
+
specialistWarnings
|
|
1067
|
+
);
|
|
1068
|
+
const specialistExecutionSurface = createExecuteAppFunction({
|
|
1069
|
+
functionsRegistry: specialistRegistry,
|
|
1070
|
+
backendFunctions: specialistBackendFunctions,
|
|
1071
|
+
executeBackendFunction: options.executeBackendFunction,
|
|
1072
|
+
context: options
|
|
1073
|
+
});
|
|
1074
|
+
const specialistFunctionTools = createFunctionTools({
|
|
1075
|
+
functionsRegistry: specialistRegistry,
|
|
1076
|
+
backendFunctions: specialistBackendFunctions,
|
|
1077
|
+
executeAppFunction: specialistExecutionSurface.executeAppFunction,
|
|
1078
|
+
includeDirectAliases: false
|
|
1079
|
+
});
|
|
1080
|
+
specialistWarnings.push(...specialistFunctionTools.aliasWarnings);
|
|
1081
|
+
aggregatedWarnings.push(...specialistWarnings);
|
|
1082
|
+
const specialistInstructions = [
|
|
1083
|
+
runtimeAgent.instructions ?? `You are the ${runtimeAgent.name} specialist agent for this web app.`,
|
|
1084
|
+
"Allowed app functions:",
|
|
1085
|
+
...buildFunctionLines(specialistRegistry, specialistBackendFunctions),
|
|
1086
|
+
"Rules:",
|
|
1087
|
+
"- Always use execute_app_function for app actions.",
|
|
1088
|
+
"- When no arguments are needed, call execute_app_function with payload set to null.",
|
|
1089
|
+
"- Use only the functions available to this specialist agent.",
|
|
1090
|
+
"- Do not navigate unless one of your allowed functions explicitly does so.",
|
|
1091
|
+
"- Return a concise result to the main NAVAI agent."
|
|
1092
|
+
].join("\n");
|
|
1093
|
+
debugLog("creating specialist agent", {
|
|
1094
|
+
key: runtimeAgent.key,
|
|
1095
|
+
name: runtimeAgent.name,
|
|
1096
|
+
functions: [
|
|
1097
|
+
...specialistRegistry.ordered.map((item) => item.name),
|
|
1098
|
+
...specialistBackendFunctions.map((item) => item.name)
|
|
1099
|
+
]
|
|
1100
|
+
});
|
|
1101
|
+
const specialistAgent = new import_realtime.RealtimeAgent({
|
|
1102
|
+
name: runtimeAgent.name,
|
|
1103
|
+
handoffDescription: runtimeAgent.handoffDescription ?? runtimeAgent.description ?? `Delegate specialist work to ${runtimeAgent.name}.`,
|
|
1104
|
+
instructions: specialistInstructions,
|
|
1105
|
+
tools: [specialistFunctionTools.executeFunctionTool]
|
|
1106
|
+
});
|
|
1107
|
+
specialistAgents.push(specialistAgent);
|
|
1108
|
+
specialistLines.push(
|
|
1109
|
+
`- ${runtimeAgent.name}: ${runtimeAgent.description ?? runtimeAgent.handoffDescription ?? "Specialist agent available by delegation."}`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
975
1112
|
const instructions = [
|
|
976
|
-
options.baseInstructions ?? "You are
|
|
1113
|
+
primaryAgentConfig?.instructions ?? options.baseInstructions ?? "You are the main NAVAI voice agent embedded in a web app.",
|
|
977
1114
|
"Allowed routes:",
|
|
978
1115
|
...routeLines,
|
|
979
1116
|
"Allowed app functions:",
|
|
980
1117
|
...functionLines,
|
|
1118
|
+
"Available specialist agents:",
|
|
1119
|
+
...specialistLines.length > 0 ? specialistLines : ["- none"],
|
|
981
1120
|
"Rules:",
|
|
982
1121
|
"- If user asks to go/open a section, always call navigate_to.",
|
|
983
|
-
"- If user asks to run an internal action, call execute_app_function or the matching direct function tool.",
|
|
1122
|
+
"- If user asks to run an internal action that belongs to you, call execute_app_function or the matching direct function tool.",
|
|
1123
|
+
"- If the task clearly belongs to a specialist agent, hand off to that specialist agent.",
|
|
1124
|
+
"- Food recommendations, fast food, hamburgers, pizza, tacos, snacks, and meal suggestions belong to the food specialist.",
|
|
984
1125
|
"- Always include payload in execute_app_function. Use null when no arguments are needed.",
|
|
985
1126
|
"- For execute_app_function, pass arguments using payload.args (array).",
|
|
986
1127
|
"- For class methods, pass payload.constructorArgs and payload.methodArgs.",
|
|
@@ -988,11 +1129,23 @@ async function buildNavaiAgent(options) {
|
|
|
988
1129
|
"- If destination/action is unclear, ask a brief clarifying question."
|
|
989
1130
|
].join("\n");
|
|
990
1131
|
const agent = new import_realtime.RealtimeAgent({
|
|
991
|
-
name: options.agentName ?? "Navai Voice Agent",
|
|
1132
|
+
name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
|
|
992
1133
|
instructions,
|
|
993
|
-
|
|
1134
|
+
handoffs: specialistAgents,
|
|
1135
|
+
tools: [
|
|
1136
|
+
navigateTool,
|
|
1137
|
+
primaryFunctionTools.executeFunctionTool,
|
|
1138
|
+
...primaryFunctionTools.directFunctionTools
|
|
1139
|
+
]
|
|
1140
|
+
});
|
|
1141
|
+
debugLog("created primary agent", {
|
|
1142
|
+
name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
|
|
1143
|
+
primaryAgentKey: primaryAgentConfig?.key ?? null,
|
|
1144
|
+
directFunctions: functionsRegistry.ordered.map((item) => item.name),
|
|
1145
|
+
backendFunctions: backendFunctionsOrdered.map((item) => item.name),
|
|
1146
|
+
specialistDelegates: configuredAgents.filter((runtimeAgent) => runtimeAgent.key !== primaryAgentConfig?.key).map((runtimeAgent) => runtimeAgent.key)
|
|
994
1147
|
});
|
|
995
|
-
return { agent, warnings:
|
|
1148
|
+
return { agent, warnings: aggregatedWarnings };
|
|
996
1149
|
}
|
|
997
1150
|
|
|
998
1151
|
// src/backend.ts
|
|
@@ -1115,6 +1268,7 @@ function createNavaiBackendClient(options = {}) {
|
|
|
1115
1268
|
// src/runtime.ts
|
|
1116
1269
|
var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
|
|
1117
1270
|
var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
|
|
1271
|
+
var AGENTS_ENV_KEYS = ["NAVAI_AGENTS_FOLDERS"];
|
|
1118
1272
|
var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
|
|
1119
1273
|
async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
1120
1274
|
const warnings = [];
|
|
@@ -1124,6 +1278,7 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
|
1124
1278
|
const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
|
|
1125
1279
|
const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
|
|
1126
1280
|
const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
|
|
1281
|
+
const agentsFolders = readOptional2(options.agentsFolders) ?? readFirstOptionalEnv(options.env, AGENTS_ENV_KEYS);
|
|
1127
1282
|
const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
|
|
1128
1283
|
const routes = await resolveRoutes({
|
|
1129
1284
|
routesFile,
|
|
@@ -1135,12 +1290,23 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
|
1135
1290
|
const functionModuleLoaders = resolveFunctionModuleLoaders({
|
|
1136
1291
|
indexedLoaders,
|
|
1137
1292
|
functionsFolders,
|
|
1293
|
+
agentsFolders,
|
|
1138
1294
|
defaultFunctionsFolder,
|
|
1139
1295
|
warnings
|
|
1140
1296
|
});
|
|
1297
|
+
const agents = await resolveRuntimeAgents({
|
|
1298
|
+
indexedLoaders,
|
|
1299
|
+
functionModuleLoaders,
|
|
1300
|
+
functionsFolders,
|
|
1301
|
+
agentsFolders,
|
|
1302
|
+
defaultFunctionsFolder
|
|
1303
|
+
});
|
|
1304
|
+
const primaryAgentKey = agents.find((agent) => agent.isPrimary)?.key;
|
|
1141
1305
|
return {
|
|
1142
1306
|
routes,
|
|
1143
1307
|
functionModuleLoaders,
|
|
1308
|
+
agents,
|
|
1309
|
+
primaryAgentKey,
|
|
1144
1310
|
modelOverride,
|
|
1145
1311
|
warnings
|
|
1146
1312
|
};
|
|
@@ -1176,10 +1342,11 @@ async function resolveRoutes(input) {
|
|
|
1176
1342
|
}
|
|
1177
1343
|
function resolveFunctionModuleLoaders(input) {
|
|
1178
1344
|
const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1345
|
+
const agentFolders = parseCsvList(input.agentsFolders);
|
|
1179
1346
|
const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
|
|
1180
|
-
const matchers = tokens.map((token) => createPathMatcher(token));
|
|
1347
|
+
const matchers = tokens.map((token) => createPathMatcher(token, agentFolders));
|
|
1181
1348
|
const matchedEntries = input.indexedLoaders.filter(
|
|
1182
|
-
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
|
|
1349
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && matchers.some((matcher) => matcher(entry.normalizedPath))
|
|
1183
1350
|
);
|
|
1184
1351
|
if (matchedEntries.length > 0) {
|
|
1185
1352
|
return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
@@ -1189,9 +1356,9 @@ function resolveFunctionModuleLoaders(input) {
|
|
|
1189
1356
|
`[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
|
|
1190
1357
|
);
|
|
1191
1358
|
}
|
|
1192
|
-
const
|
|
1359
|
+
const fallbackMatcherWithAgents = createPathMatcher(input.defaultFunctionsFolder, agentFolders);
|
|
1193
1360
|
const fallbackEntries = input.indexedLoaders.filter(
|
|
1194
|
-
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") &&
|
|
1361
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && fallbackMatcherWithAgents(entry.normalizedPath)
|
|
1195
1362
|
);
|
|
1196
1363
|
return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
1197
1364
|
}
|
|
@@ -1234,7 +1401,7 @@ function buildModuleCandidates(inputPath) {
|
|
|
1234
1401
|
}
|
|
1235
1402
|
return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
|
|
1236
1403
|
}
|
|
1237
|
-
function createPathMatcher(input) {
|
|
1404
|
+
function createPathMatcher(input, agentFolders = []) {
|
|
1238
1405
|
const raw = normalizePath(input);
|
|
1239
1406
|
if (!raw) {
|
|
1240
1407
|
return () => false;
|
|
@@ -1252,8 +1419,141 @@ function createPathMatcher(input) {
|
|
|
1252
1419
|
return (path) => path === normalized;
|
|
1253
1420
|
}
|
|
1254
1421
|
const base = normalized.replace(/\/+$/, "");
|
|
1422
|
+
const normalizedAgents = agentFolders.map(normalizePathSegment).filter(Boolean);
|
|
1423
|
+
if (normalizedAgents.length > 0) {
|
|
1424
|
+
return (path) => {
|
|
1425
|
+
if (!path.startsWith(`${base}/`)) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
const suffix = path.slice(base.length + 1);
|
|
1429
|
+
const firstSegment = suffix.split("/", 1)[0] ?? "";
|
|
1430
|
+
return normalizedAgents.includes(firstSegment);
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1255
1433
|
return (path) => path === base || path.startsWith(`${base}/`);
|
|
1256
1434
|
}
|
|
1435
|
+
async function resolveRuntimeAgents(input) {
|
|
1436
|
+
const configuredAgents = parseCsvList(input.agentsFolders);
|
|
1437
|
+
if (configuredAgents.length === 0) {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
const loaderByPath = new Map(input.indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
|
|
1441
|
+
const baseDirectories = resolveAgentBaseDirectories(input.functionsFolders, input.defaultFunctionsFolder);
|
|
1442
|
+
const groupedLoaders = /* @__PURE__ */ new Map();
|
|
1443
|
+
for (const [rawPath, load] of Object.entries(input.functionModuleLoaders)) {
|
|
1444
|
+
const agentKey = extractAgentKeyFromPath(rawPath, baseDirectories, configuredAgents);
|
|
1445
|
+
if (!agentKey) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
const current = groupedLoaders.get(agentKey) ?? {};
|
|
1449
|
+
current[rawPath] = load;
|
|
1450
|
+
groupedLoaders.set(agentKey, current);
|
|
1451
|
+
}
|
|
1452
|
+
const configuredPrimaryKey = configuredAgents[0];
|
|
1453
|
+
const agents = [];
|
|
1454
|
+
for (const agentKey of configuredAgents) {
|
|
1455
|
+
const functionLoaders = groupedLoaders.get(agentKey);
|
|
1456
|
+
if (!functionLoaders || Object.keys(functionLoaders).length === 0) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const config = await loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath);
|
|
1460
|
+
agents.push({
|
|
1461
|
+
key: config.key?.trim() || agentKey,
|
|
1462
|
+
name: readOptional2(config.name) ?? humanizeAgentKey(agentKey),
|
|
1463
|
+
description: readOptional2(config.description),
|
|
1464
|
+
handoffDescription: readOptional2(config.handoffDescription) ?? readOptional2(config.description),
|
|
1465
|
+
instructions: readOptional2(config.instructions),
|
|
1466
|
+
isPrimary: config.isPrimary === true || agentKey === configuredPrimaryKey,
|
|
1467
|
+
functionModuleLoaders: functionLoaders
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
if (agents.filter((agent) => agent.isPrimary).length === 0 && agents[0]) {
|
|
1471
|
+
agents[0].isPrimary = true;
|
|
1472
|
+
}
|
|
1473
|
+
if (agents.filter((agent) => agent.isPrimary).length > 1) {
|
|
1474
|
+
let primaryAssigned = false;
|
|
1475
|
+
for (const agent of agents) {
|
|
1476
|
+
if (agent.isPrimary && !primaryAssigned) {
|
|
1477
|
+
primaryAssigned = true;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
agent.isPrimary = false;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return agents;
|
|
1484
|
+
}
|
|
1485
|
+
async function loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath) {
|
|
1486
|
+
for (const baseDirectory of baseDirectories) {
|
|
1487
|
+
const configBase = `${baseDirectory}/${agentKey}/agent.config`;
|
|
1488
|
+
const matchedLoader = buildModuleCandidates(configBase).map((candidate) => loaderByPath.get(candidate)).find(Boolean);
|
|
1489
|
+
if (!matchedLoader) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const imported = await matchedLoader.load();
|
|
1494
|
+
return readAgentModuleConfig(imported);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return {};
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return {};
|
|
1500
|
+
}
|
|
1501
|
+
function readAgentModuleConfig(moduleShape) {
|
|
1502
|
+
const candidate = readRecord(moduleShape.NAVAI_AGENT) ?? readRecord(moduleShape.agent) ?? readRecord(moduleShape.default) ?? {};
|
|
1503
|
+
return {
|
|
1504
|
+
key: readOptionalString(candidate.key),
|
|
1505
|
+
name: readOptionalString(candidate.name),
|
|
1506
|
+
description: readOptionalString(candidate.description),
|
|
1507
|
+
handoffDescription: readOptionalString(candidate.handoffDescription),
|
|
1508
|
+
instructions: readOptionalString(candidate.instructions),
|
|
1509
|
+
isPrimary: candidate.isPrimary === true
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function readRecord(value) {
|
|
1513
|
+
return value && typeof value === "object" ? value : null;
|
|
1514
|
+
}
|
|
1515
|
+
function readOptionalString(value) {
|
|
1516
|
+
return typeof value === "string" ? readOptional2(value) : void 0;
|
|
1517
|
+
}
|
|
1518
|
+
function resolveAgentBaseDirectories(functionsFolders, defaultFunctionsFolder) {
|
|
1519
|
+
const configuredTokens = functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1520
|
+
const tokens = configuredTokens.length > 0 ? configuredTokens : [defaultFunctionsFolder];
|
|
1521
|
+
return [...new Set(tokens.map(toAgentBaseDirectory).filter(Boolean))];
|
|
1522
|
+
}
|
|
1523
|
+
function toAgentBaseDirectory(input) {
|
|
1524
|
+
const raw = normalizePath(input);
|
|
1525
|
+
if (!raw) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
|
|
1529
|
+
if (normalized.includes("*") || /\.[cm]?[jt]s$/.test(normalized)) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
if (normalized.endsWith("/...")) {
|
|
1533
|
+
return normalized.slice(0, -4).replace(/\/+$/, "") || null;
|
|
1534
|
+
}
|
|
1535
|
+
return normalized.replace(/\/+$/, "") || null;
|
|
1536
|
+
}
|
|
1537
|
+
function extractAgentKeyFromPath(pathValue, baseDirectories, configuredAgents) {
|
|
1538
|
+
const normalized = normalizePath(pathValue);
|
|
1539
|
+
for (const baseDirectory of baseDirectories) {
|
|
1540
|
+
if (!normalized.startsWith(`${baseDirectory}/`)) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const suffix = normalized.slice(baseDirectory.length + 1);
|
|
1544
|
+
const firstSegment = suffix.split("/", 1)[0] ?? "";
|
|
1545
|
+
if (configuredAgents.includes(firstSegment)) {
|
|
1546
|
+
return firstSegment;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return void 0;
|
|
1550
|
+
}
|
|
1551
|
+
function humanizeAgentKey(value) {
|
|
1552
|
+
return value.split(/[_-]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
1553
|
+
}
|
|
1554
|
+
function isAgentConfigPath(pathValue) {
|
|
1555
|
+
return /\/agent\.config\.[cm]?[jt]s$/i.test(pathValue);
|
|
1556
|
+
}
|
|
1257
1557
|
function globToRegExp(pattern) {
|
|
1258
1558
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1259
1559
|
const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
|
|
@@ -1264,6 +1564,12 @@ function globToRegExp(pattern) {
|
|
|
1264
1564
|
function normalizePath(input) {
|
|
1265
1565
|
return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
|
|
1266
1566
|
}
|
|
1567
|
+
function normalizePathSegment(input) {
|
|
1568
|
+
return normalizePath(input).replace(/\//g, "");
|
|
1569
|
+
}
|
|
1570
|
+
function parseCsvList(input) {
|
|
1571
|
+
return (input ?? "").split(",").map((value) => normalizePathSegment(value)).filter(Boolean);
|
|
1572
|
+
}
|
|
1267
1573
|
function readFirstOptionalEnv(env, keys) {
|
|
1268
1574
|
if (!env) {
|
|
1269
1575
|
return void 0;
|
|
@@ -1294,6 +1600,7 @@ function toErrorMessage3(error) {
|
|
|
1294
1600
|
// src/useWebVoiceAgent.ts
|
|
1295
1601
|
var import_realtime2 = require("@openai/agents/realtime");
|
|
1296
1602
|
var import_react = require("react");
|
|
1603
|
+
var DEBUG_PREFIX2 = "[navai debug]";
|
|
1297
1604
|
function formatError(error) {
|
|
1298
1605
|
if (error instanceof Error) {
|
|
1299
1606
|
return error.message;
|
|
@@ -1307,6 +1614,13 @@ function emitWarnings(warnings) {
|
|
|
1307
1614
|
}
|
|
1308
1615
|
}
|
|
1309
1616
|
}
|
|
1617
|
+
function debugLog2(message, details) {
|
|
1618
|
+
if (details === void 0) {
|
|
1619
|
+
console.log(`${DEBUG_PREFIX2} ${message}`);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
console.log(`${DEBUG_PREFIX2} ${message}`, details);
|
|
1623
|
+
}
|
|
1310
1624
|
function useWebVoiceAgent(options) {
|
|
1311
1625
|
const sessionRef = (0, import_react.useRef)(null);
|
|
1312
1626
|
const attachedRealtimeSessionRef = (0, import_react.useRef)(null);
|
|
@@ -1317,6 +1631,7 @@ function useWebVoiceAgent(options) {
|
|
|
1317
1631
|
env: options.env,
|
|
1318
1632
|
routesFile: options.routesFile,
|
|
1319
1633
|
functionsFolders: options.functionsFolders,
|
|
1634
|
+
agentsFolders: options.agentsFolders,
|
|
1320
1635
|
modelOverride: options.modelOverride,
|
|
1321
1636
|
defaultRoutesFile: options.defaultRoutesFile,
|
|
1322
1637
|
defaultFunctionsFolder: options.defaultFunctionsFolder
|
|
@@ -1325,6 +1640,7 @@ function useWebVoiceAgent(options) {
|
|
|
1325
1640
|
options.defaultFunctionsFolder,
|
|
1326
1641
|
options.defaultRoutes,
|
|
1327
1642
|
options.defaultRoutesFile,
|
|
1643
|
+
options.agentsFolders,
|
|
1328
1644
|
options.env,
|
|
1329
1645
|
options.functionsFolders,
|
|
1330
1646
|
options.modelOverride,
|
|
@@ -1371,6 +1687,45 @@ function useWebVoiceAgent(options) {
|
|
|
1371
1687
|
const attachSessionAudioListeners = (0, import_react.useCallback)(
|
|
1372
1688
|
(session) => {
|
|
1373
1689
|
detachSessionAudioListeners();
|
|
1690
|
+
session.on("agent_start", (_context, agent, turnInput) => {
|
|
1691
|
+
debugLog2("session agent_start", {
|
|
1692
|
+
agent: agent.name,
|
|
1693
|
+
turnInputCount: Array.isArray(turnInput) ? turnInput.length : 0
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
session.on("agent_end", (_context, agent, output) => {
|
|
1697
|
+
debugLog2("session agent_end", {
|
|
1698
|
+
agent: agent.name,
|
|
1699
|
+
output
|
|
1700
|
+
});
|
|
1701
|
+
});
|
|
1702
|
+
session.on("agent_handoff", (_context, fromAgent, toAgent) => {
|
|
1703
|
+
debugLog2("session agent_handoff", {
|
|
1704
|
+
from: fromAgent.name,
|
|
1705
|
+
to: toAgent.name
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
session.on("agent_tool_start", (_context, agent, tool2, details) => {
|
|
1709
|
+
debugLog2("session agent_tool_start", {
|
|
1710
|
+
agent: agent.name,
|
|
1711
|
+
tool: tool2.name,
|
|
1712
|
+
toolCall: details.toolCall
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
session.on("agent_tool_end", (_context, agent, tool2, result, details) => {
|
|
1716
|
+
debugLog2("session agent_tool_end", {
|
|
1717
|
+
agent: agent.name,
|
|
1718
|
+
tool: tool2.name,
|
|
1719
|
+
result,
|
|
1720
|
+
toolCall: details.toolCall
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
session.on("history_added", (item) => {
|
|
1724
|
+
debugLog2("session history_added", item);
|
|
1725
|
+
});
|
|
1726
|
+
session.on("error", (sessionError) => {
|
|
1727
|
+
debugLog2("session error", sessionError);
|
|
1728
|
+
});
|
|
1374
1729
|
session.on("audio_start", handleSessionAudioStart);
|
|
1375
1730
|
session.on("audio_stopped", handleSessionAudioStopped);
|
|
1376
1731
|
session.on("audio_interrupted", handleSessionAudioInterrupted);
|
|
@@ -1409,6 +1764,17 @@ function useWebVoiceAgent(options) {
|
|
|
1409
1764
|
setAgentVoiceStateIfChanged("idle");
|
|
1410
1765
|
try {
|
|
1411
1766
|
const runtimeConfig = await runtimeConfigPromise;
|
|
1767
|
+
debugLog2("resolved runtime config", {
|
|
1768
|
+
routes: runtimeConfig.routes.map((route) => route.path),
|
|
1769
|
+
functionModules: Object.keys(runtimeConfig.functionModuleLoaders),
|
|
1770
|
+
agents: runtimeConfig.agents.map((agent2) => ({
|
|
1771
|
+
key: agent2.key,
|
|
1772
|
+
name: agent2.name,
|
|
1773
|
+
isPrimary: agent2.isPrimary,
|
|
1774
|
+
functionModules: Object.keys(agent2.functionModuleLoaders)
|
|
1775
|
+
})),
|
|
1776
|
+
warnings: runtimeConfig.warnings
|
|
1777
|
+
});
|
|
1412
1778
|
const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
|
|
1413
1779
|
const secretPayload = await backendClient.createClientSecret(requestPayload);
|
|
1414
1780
|
const backendFunctionsResult = await backendClient.listFunctions();
|
|
@@ -1416,6 +1782,8 @@ function useWebVoiceAgent(options) {
|
|
|
1416
1782
|
navigate: options.navigate,
|
|
1417
1783
|
routes: runtimeConfig.routes,
|
|
1418
1784
|
functionModuleLoaders: runtimeConfig.functionModuleLoaders,
|
|
1785
|
+
agents: runtimeConfig.agents,
|
|
1786
|
+
primaryAgentKey: runtimeConfig.primaryAgentKey,
|
|
1419
1787
|
backendFunctions: backendFunctionsResult.functions,
|
|
1420
1788
|
executeBackendFunction: backendClient.executeFunction
|
|
1421
1789
|
});
|
|
@@ -1431,6 +1799,7 @@ function useWebVoiceAgent(options) {
|
|
|
1431
1799
|
setStatus("connected");
|
|
1432
1800
|
} catch (startError) {
|
|
1433
1801
|
const message = formatError(startError);
|
|
1802
|
+
debugLog2("session start failed", { message, error: startError });
|
|
1434
1803
|
setError(message);
|
|
1435
1804
|
setStatus("error");
|
|
1436
1805
|
setAgentVoiceStateIfChanged("idle");
|