@navai/voice-frontend 0.1.0
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.md +95 -0
- package/dist/index.cjs +717 -0
- package/dist/index.d.cts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +685 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// src/agent.ts
|
|
2
|
+
import { RealtimeAgent, tool } from "@openai/agents/realtime";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// src/functions.ts
|
|
6
|
+
function toErrorMessage(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function normalizeName(value) {
|
|
10
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function stripKnownExtensions(filename) {
|
|
13
|
+
let output = filename;
|
|
14
|
+
output = output.replace(/\.(ts|js)$/i, "");
|
|
15
|
+
output = output.replace(/\.fn$/i, "");
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
function getModuleStem(path) {
|
|
19
|
+
const parts = path.split("/");
|
|
20
|
+
const last = parts[parts.length - 1] ?? "module";
|
|
21
|
+
const stem = stripKnownExtensions(last);
|
|
22
|
+
return stem || "module";
|
|
23
|
+
}
|
|
24
|
+
function isClassConstructor(value) {
|
|
25
|
+
if (typeof value !== "function") return false;
|
|
26
|
+
const source = Function.prototype.toString.call(value);
|
|
27
|
+
return /^\s*class\s/.test(source);
|
|
28
|
+
}
|
|
29
|
+
function isCallable(value) {
|
|
30
|
+
return typeof value === "function";
|
|
31
|
+
}
|
|
32
|
+
function readArray(value) {
|
|
33
|
+
return Array.isArray(value) ? value : [];
|
|
34
|
+
}
|
|
35
|
+
function buildInvocationArgs(payload, context, targetArity) {
|
|
36
|
+
const directArgs = readArray(payload.args ?? payload.arguments);
|
|
37
|
+
const args = directArgs.length > 0 ? [...directArgs] : [];
|
|
38
|
+
if (args.length === 0 && "value" in payload) {
|
|
39
|
+
args.push(payload.value);
|
|
40
|
+
} else if (args.length === 0 && Object.keys(payload).length > 0) {
|
|
41
|
+
args.push(payload);
|
|
42
|
+
}
|
|
43
|
+
if (targetArity > args.length) {
|
|
44
|
+
args.push(context);
|
|
45
|
+
}
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
function makeFunctionDefinition(name, description, source, callable) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description,
|
|
52
|
+
source,
|
|
53
|
+
run: async (payload, context) => {
|
|
54
|
+
const args = buildInvocationArgs(payload, context, callable.length);
|
|
55
|
+
return await callable(...args);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function makeClassMethodDefinition(name, description, source, ClassRef, method) {
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
description,
|
|
63
|
+
source,
|
|
64
|
+
run: async (payload, context) => {
|
|
65
|
+
const constructorArgs = readArray(payload.constructorArgs);
|
|
66
|
+
const methodArgsFromPayload = readArray(payload.methodArgs);
|
|
67
|
+
const args = methodArgsFromPayload.length > 0 ? [...methodArgsFromPayload] : buildInvocationArgs(payload, context, method.length);
|
|
68
|
+
const instance = new ClassRef(...constructorArgs);
|
|
69
|
+
const boundMethod = method.bind(instance);
|
|
70
|
+
if (methodArgsFromPayload.length > 0 && method.length > args.length) {
|
|
71
|
+
args.push(context);
|
|
72
|
+
}
|
|
73
|
+
return await boundMethod(...args);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function uniqueName(baseName, usedNames) {
|
|
78
|
+
const candidate = normalizeName(baseName) || "fn";
|
|
79
|
+
if (!usedNames.has(candidate)) {
|
|
80
|
+
usedNames.add(candidate);
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
let index = 2;
|
|
84
|
+
while (usedNames.has(`${candidate}_${index}`)) {
|
|
85
|
+
index += 1;
|
|
86
|
+
}
|
|
87
|
+
const finalName = `${candidate}_${index}`;
|
|
88
|
+
usedNames.add(finalName);
|
|
89
|
+
return finalName;
|
|
90
|
+
}
|
|
91
|
+
function collectFromClass(path, exportName, ClassRef, usedNames) {
|
|
92
|
+
const defs = [];
|
|
93
|
+
const warnings = [];
|
|
94
|
+
const className = ClassRef.name || getModuleStem(path);
|
|
95
|
+
const proto = ClassRef.prototype;
|
|
96
|
+
const methodNames = Object.getOwnPropertyNames(proto).filter((name) => name !== "constructor");
|
|
97
|
+
if (methodNames.length === 0) {
|
|
98
|
+
warnings.push(`[navai] Ignored ${path}#${exportName}: class has no callable instance methods.`);
|
|
99
|
+
return { defs, warnings };
|
|
100
|
+
}
|
|
101
|
+
for (const methodName of methodNames) {
|
|
102
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
103
|
+
const method = descriptor?.value;
|
|
104
|
+
if (!isCallable(method)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const rawName = `${className}_${methodName}`;
|
|
108
|
+
const finalName = uniqueName(rawName, usedNames);
|
|
109
|
+
if (finalName !== normalizeName(rawName)) {
|
|
110
|
+
warnings.push(`[navai] Renamed duplicated function "${rawName}" to "${finalName}".`);
|
|
111
|
+
}
|
|
112
|
+
defs.push(
|
|
113
|
+
makeClassMethodDefinition(
|
|
114
|
+
finalName,
|
|
115
|
+
`Call class method ${className}.${methodName}().`,
|
|
116
|
+
`${path}#${exportName}.${methodName}`,
|
|
117
|
+
ClassRef,
|
|
118
|
+
method
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return { defs, warnings };
|
|
123
|
+
}
|
|
124
|
+
function collectFromObject(path, exportName, value, usedNames) {
|
|
125
|
+
const defs = [];
|
|
126
|
+
const warnings = [];
|
|
127
|
+
const callableEntries = Object.entries(value);
|
|
128
|
+
if (callableEntries.length === 0) {
|
|
129
|
+
warnings.push(`[navai] Ignored ${path}#${exportName}: exported object has no callable members.`);
|
|
130
|
+
return { defs, warnings };
|
|
131
|
+
}
|
|
132
|
+
const stem = getModuleStem(path);
|
|
133
|
+
for (const [memberName, member] of callableEntries) {
|
|
134
|
+
if (!isCallable(member)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const rawName = `${exportName === "default" ? stem : exportName}_${memberName}`;
|
|
138
|
+
const finalName = uniqueName(rawName, usedNames);
|
|
139
|
+
if (finalName !== normalizeName(rawName)) {
|
|
140
|
+
warnings.push(`[navai] Renamed duplicated function "${rawName}" to "${finalName}".`);
|
|
141
|
+
}
|
|
142
|
+
defs.push(
|
|
143
|
+
makeFunctionDefinition(
|
|
144
|
+
finalName,
|
|
145
|
+
`Call exported object member ${exportName}.${memberName}().`,
|
|
146
|
+
`${path}#${exportName}.${memberName}`,
|
|
147
|
+
member
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (defs.length === 0) {
|
|
152
|
+
warnings.push(`[navai] Ignored ${path}#${exportName}: exported object has no callable members.`);
|
|
153
|
+
}
|
|
154
|
+
return { defs, warnings };
|
|
155
|
+
}
|
|
156
|
+
function collectFromExportValue(path, exportName, value, usedNames) {
|
|
157
|
+
if (isClassConstructor(value)) {
|
|
158
|
+
return collectFromClass(path, exportName, value, usedNames);
|
|
159
|
+
}
|
|
160
|
+
if (isCallable(value)) {
|
|
161
|
+
const fnName = exportName === "default" ? value.name || getModuleStem(path) : exportName;
|
|
162
|
+
const finalName = uniqueName(fnName, usedNames);
|
|
163
|
+
const warning = finalName !== normalizeName(fnName) ? [`[navai] Renamed duplicated function "${fnName}" to "${finalName}".`] : [];
|
|
164
|
+
return {
|
|
165
|
+
defs: [
|
|
166
|
+
makeFunctionDefinition(
|
|
167
|
+
finalName,
|
|
168
|
+
`Call exported function ${exportName}.`,
|
|
169
|
+
`${path}#${exportName}`,
|
|
170
|
+
value
|
|
171
|
+
)
|
|
172
|
+
],
|
|
173
|
+
warnings: warning
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (value && typeof value === "object") {
|
|
177
|
+
return collectFromObject(path, exportName, value, usedNames);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
defs: [],
|
|
181
|
+
warnings: []
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function loadNavaiFunctions(functionModuleLoaders) {
|
|
185
|
+
const byName = /* @__PURE__ */ new Map();
|
|
186
|
+
const ordered = [];
|
|
187
|
+
const warnings = [];
|
|
188
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
189
|
+
const entries = Object.entries(functionModuleLoaders).filter(([path]) => !path.endsWith(".d.ts")).sort(([a], [b]) => a.localeCompare(b));
|
|
190
|
+
for (const [path, load] of entries) {
|
|
191
|
+
try {
|
|
192
|
+
const imported = await load();
|
|
193
|
+
const exportEntries = Object.entries(imported);
|
|
194
|
+
if (exportEntries.length === 0) {
|
|
195
|
+
warnings.push(`[navai] Ignored ${path}: module has no exports.`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const defsBeforeModule = ordered.length;
|
|
199
|
+
for (const [exportName, value] of exportEntries) {
|
|
200
|
+
const { defs, warnings: exportWarnings } = collectFromExportValue(path, exportName, value, usedNames);
|
|
201
|
+
warnings.push(...exportWarnings);
|
|
202
|
+
for (const definition of defs) {
|
|
203
|
+
byName.set(definition.name, definition);
|
|
204
|
+
ordered.push(definition);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (ordered.length === defsBeforeModule) {
|
|
208
|
+
warnings.push(`[navai] Ignored ${path}: module has no callable exports.`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
warnings.push(`[navai] Failed to load ${path}: ${toErrorMessage(error)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { byName, ordered, warnings };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/routes.ts
|
|
218
|
+
function normalize(value) {
|
|
219
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().toLowerCase();
|
|
220
|
+
}
|
|
221
|
+
function resolveNavaiRoute(input, routes = []) {
|
|
222
|
+
const normalized = normalize(input);
|
|
223
|
+
const direct = routes.find((route) => normalize(route.path) === normalized);
|
|
224
|
+
if (direct) return direct.path;
|
|
225
|
+
for (const route of routes) {
|
|
226
|
+
if (normalize(route.name) === normalized) return route.path;
|
|
227
|
+
if (route.synonyms?.some((synonym) => normalize(synonym) === normalized)) return route.path;
|
|
228
|
+
}
|
|
229
|
+
for (const route of routes) {
|
|
230
|
+
if (normalized.includes(normalize(route.name))) return route.path;
|
|
231
|
+
if (route.synonyms?.some((synonym) => normalized.includes(normalize(synonym)))) return route.path;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function getNavaiRoutePromptLines(routes = []) {
|
|
236
|
+
return routes.map((route) => {
|
|
237
|
+
const synonyms = route.synonyms?.length ? `, aliases: ${route.synonyms.join(", ")}` : "";
|
|
238
|
+
return `- ${route.name} (${route.path})${synonyms}`;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/agent.ts
|
|
243
|
+
function toErrorMessage2(error) {
|
|
244
|
+
return error instanceof Error ? error.message : String(error);
|
|
245
|
+
}
|
|
246
|
+
async function buildNavaiAgent(options) {
|
|
247
|
+
const functionsRegistry = await loadNavaiFunctions(options.functionModuleLoaders ?? {});
|
|
248
|
+
const backendWarnings = [];
|
|
249
|
+
const backendFunctionsByName = /* @__PURE__ */ new Map();
|
|
250
|
+
const backendFunctionsOrdered = [];
|
|
251
|
+
for (const backendFunction of options.backendFunctions ?? []) {
|
|
252
|
+
const name = backendFunction.name.trim().toLowerCase();
|
|
253
|
+
if (!name) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (functionsRegistry.byName.has(name)) {
|
|
257
|
+
backendWarnings.push(
|
|
258
|
+
`[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
|
|
259
|
+
);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (backendFunctionsByName.has(name)) {
|
|
263
|
+
backendWarnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const normalizedDefinition = {
|
|
267
|
+
...backendFunction,
|
|
268
|
+
name
|
|
269
|
+
};
|
|
270
|
+
backendFunctionsByName.set(name, normalizedDefinition);
|
|
271
|
+
backendFunctionsOrdered.push(normalizedDefinition);
|
|
272
|
+
}
|
|
273
|
+
const availableFunctionNames = [
|
|
274
|
+
...functionsRegistry.ordered.map((item) => item.name),
|
|
275
|
+
...backendFunctionsOrdered.map((item) => item.name)
|
|
276
|
+
];
|
|
277
|
+
const navigateTool = tool({
|
|
278
|
+
name: "navigate_to",
|
|
279
|
+
description: "Navigate to an allowed route in the current app.",
|
|
280
|
+
parameters: z.object({
|
|
281
|
+
target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
|
|
282
|
+
}),
|
|
283
|
+
execute: async ({ target }) => {
|
|
284
|
+
const path = resolveNavaiRoute(target, options.routes);
|
|
285
|
+
if (!path) {
|
|
286
|
+
return { ok: false, error: "Unknown or disallowed route." };
|
|
287
|
+
}
|
|
288
|
+
options.navigate(path);
|
|
289
|
+
return { ok: true, path };
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const executeFunctionTool = tool({
|
|
293
|
+
name: "execute_app_function",
|
|
294
|
+
description: "Execute an allowed internal app function by name.",
|
|
295
|
+
parameters: z.object({
|
|
296
|
+
function_name: z.string().min(1).describe("Allowed function name from the list."),
|
|
297
|
+
payload: z.record(z.string(), z.unknown()).nullable().describe(
|
|
298
|
+
"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."
|
|
299
|
+
)
|
|
300
|
+
}),
|
|
301
|
+
execute: async ({ function_name, payload }) => {
|
|
302
|
+
const requested = function_name.trim().toLowerCase();
|
|
303
|
+
const frontendDefinition = functionsRegistry.byName.get(requested);
|
|
304
|
+
if (frontendDefinition) {
|
|
305
|
+
try {
|
|
306
|
+
const result = await frontendDefinition.run(payload ?? {}, options);
|
|
307
|
+
return { ok: true, function_name: frontendDefinition.name, source: frontendDefinition.source, result };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
function_name: frontendDefinition.name,
|
|
312
|
+
error: "Function execution failed.",
|
|
313
|
+
details: toErrorMessage2(error)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const backendDefinition = backendFunctionsByName.get(requested);
|
|
318
|
+
if (!backendDefinition) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
error: "Unknown or disallowed function.",
|
|
322
|
+
available_functions: availableFunctionNames
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (!options.executeBackendFunction) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
function_name: backendDefinition.name,
|
|
329
|
+
error: "Backend function execution is not configured."
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const result = await options.executeBackendFunction({
|
|
334
|
+
functionName: backendDefinition.name,
|
|
335
|
+
payload: payload ?? null
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
function_name: backendDefinition.name,
|
|
340
|
+
source: backendDefinition.source ?? "backend",
|
|
341
|
+
result
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
function_name: backendDefinition.name,
|
|
347
|
+
error: "Function execution failed.",
|
|
348
|
+
details: toErrorMessage2(error)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const routeLines = getNavaiRoutePromptLines(options.routes);
|
|
354
|
+
const functionLines = functionsRegistry.ordered.length + backendFunctionsOrdered.length > 0 ? [
|
|
355
|
+
...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
|
|
356
|
+
...backendFunctionsOrdered.map(
|
|
357
|
+
(item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
|
|
358
|
+
)
|
|
359
|
+
] : ["- none"];
|
|
360
|
+
const instructions = [
|
|
361
|
+
options.baseInstructions ?? "You are a voice assistant embedded in a web app.",
|
|
362
|
+
"Allowed routes:",
|
|
363
|
+
...routeLines,
|
|
364
|
+
"Allowed app functions:",
|
|
365
|
+
...functionLines,
|
|
366
|
+
"Rules:",
|
|
367
|
+
"- If user asks to go/open a section, always call navigate_to.",
|
|
368
|
+
"- If user asks to run an internal action, always call execute_app_function.",
|
|
369
|
+
"- Always include payload in execute_app_function. Use null when no arguments are needed.",
|
|
370
|
+
"- For execute_app_function, pass arguments using payload.args (array).",
|
|
371
|
+
"- For class methods, pass payload.constructorArgs and payload.methodArgs.",
|
|
372
|
+
"- Never invent routes or function names that are not listed.",
|
|
373
|
+
"- If destination/action is unclear, ask a brief clarifying question."
|
|
374
|
+
].join("\n");
|
|
375
|
+
const agent = new RealtimeAgent({
|
|
376
|
+
name: options.agentName ?? "Navai Voice Agent",
|
|
377
|
+
instructions,
|
|
378
|
+
tools: [navigateTool, executeFunctionTool]
|
|
379
|
+
});
|
|
380
|
+
return { agent, warnings: [...functionsRegistry.warnings, ...backendWarnings] };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/backend.ts
|
|
384
|
+
var DEFAULT_API_BASE_URL = "http://localhost:3000";
|
|
385
|
+
var DEFAULT_CLIENT_SECRET_PATH = "/navai/realtime/client-secret";
|
|
386
|
+
var DEFAULT_FUNCTIONS_LIST_PATH = "/navai/functions";
|
|
387
|
+
var DEFAULT_FUNCTIONS_EXECUTE_PATH = "/navai/functions/execute";
|
|
388
|
+
function readOptional(value) {
|
|
389
|
+
const trimmed = value?.trim();
|
|
390
|
+
return trimmed ? trimmed : void 0;
|
|
391
|
+
}
|
|
392
|
+
function joinUrl(baseUrl, path) {
|
|
393
|
+
const cleanBase = baseUrl.replace(/\/+$/, "");
|
|
394
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
395
|
+
return `${cleanBase}${cleanPath}`;
|
|
396
|
+
}
|
|
397
|
+
function isRecord(value) {
|
|
398
|
+
return Boolean(value && typeof value === "object");
|
|
399
|
+
}
|
|
400
|
+
async function readTextSafe(response) {
|
|
401
|
+
try {
|
|
402
|
+
return await response.text();
|
|
403
|
+
} catch {
|
|
404
|
+
return `HTTP ${response.status}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async function readJsonSafe(response) {
|
|
408
|
+
try {
|
|
409
|
+
return await response.json();
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function createNavaiBackendClient(options = {}) {
|
|
415
|
+
const apiBaseUrl = readOptional(options.apiBaseUrl) ?? readOptional(options.env?.NAVAI_API_URL) ?? DEFAULT_API_BASE_URL;
|
|
416
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
417
|
+
const clientSecretUrl = joinUrl(apiBaseUrl, options.clientSecretPath ?? DEFAULT_CLIENT_SECRET_PATH);
|
|
418
|
+
const functionsListUrl = joinUrl(apiBaseUrl, options.functionsListPath ?? DEFAULT_FUNCTIONS_LIST_PATH);
|
|
419
|
+
const functionsExecuteUrl = joinUrl(apiBaseUrl, options.functionsExecutePath ?? DEFAULT_FUNCTIONS_EXECUTE_PATH);
|
|
420
|
+
async function createClientSecret(input = {}) {
|
|
421
|
+
const response = await fetchImpl(clientSecretUrl, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
body: JSON.stringify(input)
|
|
425
|
+
});
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
throw new Error(await readTextSafe(response));
|
|
428
|
+
}
|
|
429
|
+
const payload = await readJsonSafe(response);
|
|
430
|
+
if (!isRecord(payload) || typeof payload.value !== "string") {
|
|
431
|
+
throw new Error("Invalid client-secret response.");
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
value: payload.value,
|
|
435
|
+
expires_at: typeof payload.expires_at === "number" ? payload.expires_at : void 0
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function listFunctions() {
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetchImpl(functionsListUrl);
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
return {
|
|
443
|
+
functions: [],
|
|
444
|
+
warnings: [`[navai] Failed to load backend functions (${response.status}).`]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const payload = await readJsonSafe(response);
|
|
448
|
+
if (!isRecord(payload)) {
|
|
449
|
+
return {
|
|
450
|
+
functions: [],
|
|
451
|
+
warnings: ["[navai] Failed to parse backend functions response."]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const rawItems = Array.isArray(payload.items) ? payload.items : [];
|
|
455
|
+
const rawWarnings = Array.isArray(payload.warnings) ? payload.warnings : [];
|
|
456
|
+
const functions = rawItems.filter((item) => isRecord(item)).map((item) => ({
|
|
457
|
+
name: typeof item.name === "string" ? item.name : "",
|
|
458
|
+
description: typeof item.description === "string" ? item.description : void 0,
|
|
459
|
+
source: typeof item.source === "string" ? item.source : void 0
|
|
460
|
+
})).filter((item) => item.name.trim().length > 0);
|
|
461
|
+
const warnings = rawWarnings.filter((item) => typeof item === "string");
|
|
462
|
+
return { functions, warnings };
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
465
|
+
return {
|
|
466
|
+
functions: [],
|
|
467
|
+
warnings: [`[navai] Failed to load backend functions: ${message}`]
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const executeFunction = async (input) => {
|
|
472
|
+
const response = await fetchImpl(functionsExecuteUrl, {
|
|
473
|
+
method: "POST",
|
|
474
|
+
headers: { "Content-Type": "application/json" },
|
|
475
|
+
body: JSON.stringify({
|
|
476
|
+
function_name: input.functionName,
|
|
477
|
+
payload: input.payload
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
throw new Error(await readTextSafe(response));
|
|
482
|
+
}
|
|
483
|
+
const payload = await readJsonSafe(response);
|
|
484
|
+
if (!isRecord(payload)) {
|
|
485
|
+
throw new Error("Invalid backend function response.");
|
|
486
|
+
}
|
|
487
|
+
if (payload.ok !== true) {
|
|
488
|
+
const details = typeof payload.details === "string" ? payload.details : typeof payload.error === "string" ? payload.error : "Backend function failed.";
|
|
489
|
+
throw new Error(details);
|
|
490
|
+
}
|
|
491
|
+
return payload.result;
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
createClientSecret,
|
|
495
|
+
listFunctions,
|
|
496
|
+
executeFunction
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/runtime.ts
|
|
501
|
+
var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
|
|
502
|
+
var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
|
|
503
|
+
var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
|
|
504
|
+
async function resolveNavaiFrontendRuntimeConfig(options) {
|
|
505
|
+
const warnings = [];
|
|
506
|
+
const indexedLoaders = toIndexedLoaders(options.moduleLoaders);
|
|
507
|
+
const loaderByPath = new Map(indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
|
|
508
|
+
const defaultRoutesFile = options.defaultRoutesFile ?? "src/ai/routes.ts";
|
|
509
|
+
const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
|
|
510
|
+
const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
|
|
511
|
+
const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
|
|
512
|
+
const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
|
|
513
|
+
const routes = await resolveRoutes({
|
|
514
|
+
routesFile,
|
|
515
|
+
defaultRoutesFile,
|
|
516
|
+
defaultRoutes: options.defaultRoutes,
|
|
517
|
+
loaderByPath,
|
|
518
|
+
warnings
|
|
519
|
+
});
|
|
520
|
+
const functionModuleLoaders = resolveFunctionModuleLoaders({
|
|
521
|
+
indexedLoaders,
|
|
522
|
+
functionsFolders,
|
|
523
|
+
defaultFunctionsFolder,
|
|
524
|
+
warnings
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
routes,
|
|
528
|
+
functionModuleLoaders,
|
|
529
|
+
modelOverride,
|
|
530
|
+
warnings
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async function resolveRoutes(input) {
|
|
534
|
+
const candidates = buildModuleCandidates(input.routesFile);
|
|
535
|
+
if (candidates.includes(input.defaultRoutesFile)) {
|
|
536
|
+
return input.defaultRoutes;
|
|
537
|
+
}
|
|
538
|
+
const matchedLoader = candidates.map((candidate) => input.loaderByPath.get(candidate)).find(Boolean);
|
|
539
|
+
if (!matchedLoader) {
|
|
540
|
+
input.warnings.push(
|
|
541
|
+
`[navai] Route module "${input.routesFile}" was not found. Falling back to "${input.defaultRoutesFile}".`
|
|
542
|
+
);
|
|
543
|
+
return input.defaultRoutes;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const imported = await matchedLoader.load();
|
|
547
|
+
const loadedRoutes = readRouteItems(imported);
|
|
548
|
+
if (!loadedRoutes) {
|
|
549
|
+
input.warnings.push(
|
|
550
|
+
`[navai] Route module "${input.routesFile}" must export NavaiRoute[] (NAVAI_ROUTE_ITEMS or default). Falling back to "${input.defaultRoutesFile}".`
|
|
551
|
+
);
|
|
552
|
+
return input.defaultRoutes;
|
|
553
|
+
}
|
|
554
|
+
return loadedRoutes;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
input.warnings.push(
|
|
557
|
+
`[navai] Failed to load route module "${input.routesFile}": ${toErrorMessage3(error)}. Falling back to "${input.defaultRoutesFile}".`
|
|
558
|
+
);
|
|
559
|
+
return input.defaultRoutes;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function resolveFunctionModuleLoaders(input) {
|
|
563
|
+
const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
|
|
564
|
+
const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
|
|
565
|
+
const matchers = tokens.map((token) => createPathMatcher(token));
|
|
566
|
+
const matchedEntries = input.indexedLoaders.filter(
|
|
567
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
|
|
568
|
+
);
|
|
569
|
+
if (matchedEntries.length > 0) {
|
|
570
|
+
return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
571
|
+
}
|
|
572
|
+
if (configuredTokens.length > 0) {
|
|
573
|
+
input.warnings.push(
|
|
574
|
+
`[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const fallbackMatcher = createPathMatcher(input.defaultFunctionsFolder);
|
|
578
|
+
const fallbackEntries = input.indexedLoaders.filter(
|
|
579
|
+
(entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && fallbackMatcher(entry.normalizedPath)
|
|
580
|
+
);
|
|
581
|
+
return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
|
|
582
|
+
}
|
|
583
|
+
function toIndexedLoaders(loaders) {
|
|
584
|
+
return Object.entries(loaders).map(([rawPath, load]) => ({
|
|
585
|
+
rawPath,
|
|
586
|
+
normalizedPath: normalizePath(rawPath),
|
|
587
|
+
load
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
function readRouteItems(moduleShape) {
|
|
591
|
+
const candidate = moduleShape.NAVAI_ROUTE_ITEMS ?? moduleShape.NAVAI_ROUTES ?? moduleShape.APP_ROUTES ?? moduleShape.routes ?? moduleShape.default;
|
|
592
|
+
if (!Array.isArray(candidate)) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (!candidate.every(isNavaiRoute)) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return candidate;
|
|
599
|
+
}
|
|
600
|
+
function isNavaiRoute(value) {
|
|
601
|
+
if (!value || typeof value !== "object") {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
const route = value;
|
|
605
|
+
if (typeof route.name !== "string" || typeof route.path !== "string" || typeof route.description !== "string") {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (!Array.isArray(route.synonyms) && route.synonyms !== void 0) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
return route.synonyms ? route.synonyms.every((item) => typeof item === "string") : true;
|
|
612
|
+
}
|
|
613
|
+
function buildModuleCandidates(inputPath) {
|
|
614
|
+
const normalized = normalizePath(inputPath);
|
|
615
|
+
const srcPrefixed = normalized.startsWith("src/") ? normalized : `src/${normalized}`;
|
|
616
|
+
const hasExtension = /\.[cm]?[jt]s$/.test(srcPrefixed);
|
|
617
|
+
if (hasExtension) {
|
|
618
|
+
return [srcPrefixed];
|
|
619
|
+
}
|
|
620
|
+
return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
|
|
621
|
+
}
|
|
622
|
+
function createPathMatcher(input) {
|
|
623
|
+
const raw = normalizePath(input);
|
|
624
|
+
if (!raw) {
|
|
625
|
+
return () => false;
|
|
626
|
+
}
|
|
627
|
+
const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
|
|
628
|
+
if (normalized.endsWith("/...")) {
|
|
629
|
+
const base2 = normalized.slice(0, -4).replace(/\/+$/, "");
|
|
630
|
+
return (path) => path.startsWith(`${base2}/`);
|
|
631
|
+
}
|
|
632
|
+
if (normalized.includes("*")) {
|
|
633
|
+
const regexp = globToRegExp(normalized);
|
|
634
|
+
return (path) => regexp.test(path);
|
|
635
|
+
}
|
|
636
|
+
if (/\.[cm]?[jt]s$/.test(normalized)) {
|
|
637
|
+
return (path) => path === normalized;
|
|
638
|
+
}
|
|
639
|
+
const base = normalized.replace(/\/+$/, "");
|
|
640
|
+
return (path) => path === base || path.startsWith(`${base}/`);
|
|
641
|
+
}
|
|
642
|
+
function globToRegExp(pattern) {
|
|
643
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
644
|
+
const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
|
|
645
|
+
const single = wildcardSafe.replace(/\*/g, "[^/]*");
|
|
646
|
+
const output = single.replace(/___DOUBLE_STAR___/g, ".*");
|
|
647
|
+
return new RegExp(`^${output}$`);
|
|
648
|
+
}
|
|
649
|
+
function normalizePath(input) {
|
|
650
|
+
return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
|
|
651
|
+
}
|
|
652
|
+
function readFirstOptionalEnv(env, keys) {
|
|
653
|
+
if (!env) {
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
for (const key of keys) {
|
|
657
|
+
const value = readOptionalEnvValue(env, key);
|
|
658
|
+
if (value) {
|
|
659
|
+
return value;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return void 0;
|
|
663
|
+
}
|
|
664
|
+
function readOptionalEnvValue(env, key) {
|
|
665
|
+
const value = env[key];
|
|
666
|
+
if (typeof value !== "string") {
|
|
667
|
+
return void 0;
|
|
668
|
+
}
|
|
669
|
+
return readOptional2(value);
|
|
670
|
+
}
|
|
671
|
+
function readOptional2(value) {
|
|
672
|
+
const trimmed = value?.trim();
|
|
673
|
+
return trimmed ? trimmed : void 0;
|
|
674
|
+
}
|
|
675
|
+
function toErrorMessage3(error) {
|
|
676
|
+
return error instanceof Error ? error.message : String(error);
|
|
677
|
+
}
|
|
678
|
+
export {
|
|
679
|
+
buildNavaiAgent,
|
|
680
|
+
createNavaiBackendClient,
|
|
681
|
+
getNavaiRoutePromptLines,
|
|
682
|
+
loadNavaiFunctions,
|
|
683
|
+
resolveNavaiFrontendRuntimeConfig,
|
|
684
|
+
resolveNavaiRoute
|
|
685
|
+
};
|