@navai/voice-frontend 0.1.1 → 0.1.2
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 +320 -0
- package/README.es.md +320 -0
- package/README.md +320 -126
- package/dist/index.cjs +63 -2
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +63 -2
- package/package.json +4 -2
package/README.en.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# @navai/voice-frontend
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
|
|
5
|
+
<a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
Frontend package to build Navai voice agents in web applications.
|
|
9
|
+
|
|
10
|
+
It removes repeated boilerplate for:
|
|
11
|
+
|
|
12
|
+
1. Realtime client secret requests.
|
|
13
|
+
2. Route-aware navigation tools.
|
|
14
|
+
3. Dynamic local function loading.
|
|
15
|
+
4. Optional backend function bridging.
|
|
16
|
+
5. React hook lifecycle for connect/disconnect.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @navai/voice-frontend @openai/agents zod
|
|
22
|
+
npm install react
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Package Architecture
|
|
26
|
+
|
|
27
|
+
This package is intentionally split by concern:
|
|
28
|
+
|
|
29
|
+
1. `src/backend.ts`
|
|
30
|
+
HTTP client for backend routes:
|
|
31
|
+
- `POST /navai/realtime/client-secret`
|
|
32
|
+
- `GET /navai/functions`
|
|
33
|
+
- `POST /navai/functions/execute`
|
|
34
|
+
|
|
35
|
+
2. `src/runtime.ts`
|
|
36
|
+
Runtime resolver for:
|
|
37
|
+
- route module selection
|
|
38
|
+
- function module filtering by `NAVAI_FUNCTIONS_FOLDERS`
|
|
39
|
+
- optional model override
|
|
40
|
+
|
|
41
|
+
3. `src/functions.ts`
|
|
42
|
+
Local function loader:
|
|
43
|
+
- imports modules from generated loaders
|
|
44
|
+
- converts exports into normalized callable tool definitions
|
|
45
|
+
|
|
46
|
+
4. `src/agent.ts`
|
|
47
|
+
Agent builder:
|
|
48
|
+
- creates `RealtimeAgent`
|
|
49
|
+
- injects built-in tools (`navigate_to`, `execute_app_function`)
|
|
50
|
+
- optionally adds direct alias tools for each allowed function
|
|
51
|
+
|
|
52
|
+
5. `src/useWebVoiceAgent.ts`
|
|
53
|
+
React lifecycle wrapper:
|
|
54
|
+
- builds runtime config
|
|
55
|
+
- requests client secret
|
|
56
|
+
- discovers backend functions
|
|
57
|
+
- builds agent
|
|
58
|
+
- opens/closes `RealtimeSession`
|
|
59
|
+
|
|
60
|
+
6. `src/routes.ts`
|
|
61
|
+
Route matching helpers for natural language to path resolution.
|
|
62
|
+
|
|
63
|
+
## End-to-End Runtime Flow
|
|
64
|
+
|
|
65
|
+
Hook-driven runtime flow (`useWebVoiceAgent`):
|
|
66
|
+
|
|
67
|
+
1. Resolve runtime config from `moduleLoaders` + `defaultRoutes` + env/options.
|
|
68
|
+
2. Create backend client with `apiBaseUrl` or `NAVAI_API_URL`.
|
|
69
|
+
3. On `start()`:
|
|
70
|
+
- request client secret.
|
|
71
|
+
- fetch backend function list.
|
|
72
|
+
- build Navai agent with local + backend functions.
|
|
73
|
+
- connect `RealtimeSession`.
|
|
74
|
+
4. On `stop()`:
|
|
75
|
+
- close session and reset state.
|
|
76
|
+
|
|
77
|
+
State machine exposed by hook:
|
|
78
|
+
|
|
79
|
+
- `idle`
|
|
80
|
+
- `connecting`
|
|
81
|
+
- `connected`
|
|
82
|
+
- `error`
|
|
83
|
+
|
|
84
|
+
Agent voice state exposed by the hook:
|
|
85
|
+
|
|
86
|
+
- `agentVoiceState`: `idle | speaking`
|
|
87
|
+
- `isAgentSpeaking`: `boolean`
|
|
88
|
+
|
|
89
|
+
`agentVoiceState` is driven by realtime events `audio_start`, `audio_stopped`, and `audio_interrupted`.
|
|
90
|
+
|
|
91
|
+
## Public API
|
|
92
|
+
|
|
93
|
+
Main exports:
|
|
94
|
+
|
|
95
|
+
- `buildNavaiAgent(...)`
|
|
96
|
+
- `createNavaiBackendClient(...)`
|
|
97
|
+
- `resolveNavaiFrontendRuntimeConfig(...)`
|
|
98
|
+
- `loadNavaiFunctions(...)`
|
|
99
|
+
- `useWebVoiceAgent(...)`
|
|
100
|
+
- `resolveNavaiRoute(...)`
|
|
101
|
+
- `getNavaiRoutePromptLines(...)`
|
|
102
|
+
|
|
103
|
+
Useful types:
|
|
104
|
+
|
|
105
|
+
- `NavaiRoute`
|
|
106
|
+
- `NavaiFunctionDefinition`
|
|
107
|
+
- `NavaiFunctionsRegistry`
|
|
108
|
+
- `NavaiBackendFunctionDefinition`
|
|
109
|
+
- `UseWebVoiceAgentOptions`
|
|
110
|
+
|
|
111
|
+
## Tool Model and Behavior
|
|
112
|
+
|
|
113
|
+
`buildNavaiAgent` always registers:
|
|
114
|
+
|
|
115
|
+
- `navigate_to`
|
|
116
|
+
- `execute_app_function`
|
|
117
|
+
|
|
118
|
+
Optional direct alias tools:
|
|
119
|
+
|
|
120
|
+
- for each allowed function name, a direct tool can be created.
|
|
121
|
+
- reserved names are never used as direct tools (`navigate_to`, `execute_app_function`).
|
|
122
|
+
- invalid tool ids are skipped (kept accessible via `execute_app_function`).
|
|
123
|
+
|
|
124
|
+
Execution precedence:
|
|
125
|
+
|
|
126
|
+
1. Try frontend/local function first.
|
|
127
|
+
2. If missing, try backend function.
|
|
128
|
+
3. If both exist with same name, frontend wins and backend is ignored with warning.
|
|
129
|
+
|
|
130
|
+
## Dynamic Function Loading Internals
|
|
131
|
+
|
|
132
|
+
`loadNavaiFunctions` supports module export shapes:
|
|
133
|
+
|
|
134
|
+
1. Exported function.
|
|
135
|
+
2. Exported class (instance methods become functions).
|
|
136
|
+
3. Exported object (callable members become functions).
|
|
137
|
+
|
|
138
|
+
Name normalization rules:
|
|
139
|
+
|
|
140
|
+
- snake_case lowercase.
|
|
141
|
+
- invalid chars removed.
|
|
142
|
+
- collisions are renamed with suffixes (`_2`, `_3`, ...).
|
|
143
|
+
|
|
144
|
+
Argument mapping rules:
|
|
145
|
+
|
|
146
|
+
- `payload.args` or `payload.arguments` as direct args.
|
|
147
|
+
- else `payload.value` as first arg.
|
|
148
|
+
- else full payload as first arg.
|
|
149
|
+
- context appended when arity indicates one more argument.
|
|
150
|
+
|
|
151
|
+
For class methods:
|
|
152
|
+
|
|
153
|
+
- constructor args: `payload.constructorArgs`.
|
|
154
|
+
- method args: `payload.methodArgs`.
|
|
155
|
+
|
|
156
|
+
## Runtime Resolution and Env Precedence
|
|
157
|
+
|
|
158
|
+
`resolveNavaiFrontendRuntimeConfig` input priority:
|
|
159
|
+
|
|
160
|
+
1. Explicit function args.
|
|
161
|
+
2. Env object keys.
|
|
162
|
+
3. Package defaults.
|
|
163
|
+
|
|
164
|
+
Keys used:
|
|
165
|
+
|
|
166
|
+
- `NAVAI_ROUTES_FILE`
|
|
167
|
+
- `NAVAI_FUNCTIONS_FOLDERS`
|
|
168
|
+
- `NAVAI_REALTIME_MODEL`
|
|
169
|
+
|
|
170
|
+
Defaults:
|
|
171
|
+
|
|
172
|
+
- routes file: `src/ai/routes.ts`
|
|
173
|
+
- functions folder: `src/ai/functions-modules`
|
|
174
|
+
|
|
175
|
+
Path matcher formats:
|
|
176
|
+
|
|
177
|
+
- folder: `src/ai/functions-modules`
|
|
178
|
+
- recursive: `src/ai/functions-modules/...`
|
|
179
|
+
- wildcard: `src/features/*/voice-functions`
|
|
180
|
+
- explicit file: `src/ai/functions-modules/secret.ts`
|
|
181
|
+
- CSV list: `a,b,c`
|
|
182
|
+
|
|
183
|
+
Fallback behavior:
|
|
184
|
+
|
|
185
|
+
- if configured folders match no modules, warning is emitted.
|
|
186
|
+
- resolver falls back to default functions folder.
|
|
187
|
+
|
|
188
|
+
## Backend Client Behavior
|
|
189
|
+
|
|
190
|
+
`createNavaiBackendClient` base URL priority:
|
|
191
|
+
|
|
192
|
+
1. `apiBaseUrl` option.
|
|
193
|
+
2. `env.NAVAI_API_URL`.
|
|
194
|
+
3. fallback `http://localhost:3000`.
|
|
195
|
+
|
|
196
|
+
Methods:
|
|
197
|
+
|
|
198
|
+
- `createClientSecret(input?)`
|
|
199
|
+
- `listFunctions()`
|
|
200
|
+
- `executeFunction({ functionName, payload })`
|
|
201
|
+
|
|
202
|
+
Error handling:
|
|
203
|
+
|
|
204
|
+
- network/HTTP failures throw for create/execute.
|
|
205
|
+
- function listing returns warnings and empty list on failures.
|
|
206
|
+
|
|
207
|
+
## Generated Module Loader CLI
|
|
208
|
+
|
|
209
|
+
This package ships:
|
|
210
|
+
|
|
211
|
+
- `navai-generate-web-loaders`
|
|
212
|
+
|
|
213
|
+
Default command behavior:
|
|
214
|
+
|
|
215
|
+
1. Reads `.env` and process env.
|
|
216
|
+
2. Resolves `NAVAI_FUNCTIONS_FOLDERS` and `NAVAI_ROUTES_FILE`.
|
|
217
|
+
3. Selects modules only from configured function paths.
|
|
218
|
+
4. Optionally includes configured route module if it differs from default route module.
|
|
219
|
+
5. Writes `src/ai/generated-module-loaders.ts`.
|
|
220
|
+
|
|
221
|
+
Manual usage:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
navai-generate-web-loaders
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Useful flags:
|
|
228
|
+
|
|
229
|
+
- `--project-root <path>`
|
|
230
|
+
- `--src-root <path>`
|
|
231
|
+
- `--output-file <path>`
|
|
232
|
+
- `--env-file <path>`
|
|
233
|
+
- `--default-functions-folder <path>`
|
|
234
|
+
- `--default-routes-file <path>`
|
|
235
|
+
- `--type-import <module>`
|
|
236
|
+
- `--export-name <identifier>`
|
|
237
|
+
|
|
238
|
+
## Auto Setup on npm Install
|
|
239
|
+
|
|
240
|
+
Postinstall script can auto-add missing scripts in consumer app:
|
|
241
|
+
|
|
242
|
+
- `generate:module-loaders` -> `navai-generate-web-loaders`
|
|
243
|
+
- `predev` -> `npm run generate:module-loaders`
|
|
244
|
+
- `prebuild` -> `npm run generate:module-loaders`
|
|
245
|
+
- `pretypecheck` -> `npm run generate:module-loaders`
|
|
246
|
+
- `prelint` -> `npm run generate:module-loaders`
|
|
247
|
+
|
|
248
|
+
Rules:
|
|
249
|
+
|
|
250
|
+
- only missing scripts are added.
|
|
251
|
+
- existing scripts are never overwritten.
|
|
252
|
+
|
|
253
|
+
Disable auto setup:
|
|
254
|
+
|
|
255
|
+
- `NAVAI_SKIP_AUTO_SETUP=1`
|
|
256
|
+
- or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
|
|
257
|
+
|
|
258
|
+
Manual setup runner:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx navai-setup-voice-frontend
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Integration Examples
|
|
265
|
+
|
|
266
|
+
Imperative integration:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { RealtimeSession } from "@openai/agents/realtime";
|
|
270
|
+
import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
|
|
271
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
272
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
273
|
+
|
|
274
|
+
const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
|
|
275
|
+
const secret = await backend.createClientSecret();
|
|
276
|
+
const backendList = await backend.listFunctions();
|
|
277
|
+
|
|
278
|
+
const { agent, warnings } = await buildNavaiAgent({
|
|
279
|
+
navigate: (path) => router.navigate(path),
|
|
280
|
+
routes: NAVAI_ROUTE_ITEMS,
|
|
281
|
+
functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
282
|
+
backendFunctions: backendList.functions,
|
|
283
|
+
executeBackendFunction: backend.executeFunction
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
warnings.forEach((w) => console.warn(w));
|
|
287
|
+
|
|
288
|
+
const session = new RealtimeSession(agent);
|
|
289
|
+
await session.connect({ apiKey: secret.value });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
React hook integration:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { useWebVoiceAgent } from "@navai/voice-frontend";
|
|
296
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
297
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
298
|
+
|
|
299
|
+
const voice = useWebVoiceAgent({
|
|
300
|
+
navigate: (path) => router.navigate(path),
|
|
301
|
+
moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
302
|
+
defaultRoutes: NAVAI_ROUTE_ITEMS,
|
|
303
|
+
env: import.meta.env as Record<string, string | undefined>
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Operational Notes
|
|
308
|
+
|
|
309
|
+
- warnings are emitted with `console.warn` from runtime, backend list, and agent builder.
|
|
310
|
+
- unknown function execution returns structured `ok: false` payload.
|
|
311
|
+
- if route module fails to load or has invalid shape, resolver falls back to default routes.
|
|
312
|
+
|
|
313
|
+
## Related Docs
|
|
314
|
+
|
|
315
|
+
- Spanish version: `README.es.md`
|
|
316
|
+
- English version: `README.en.md`
|
|
317
|
+
- Backend package: `../voice-backend/README.md`
|
|
318
|
+
- Mobile package: `../voice-mobile/README.md`
|
|
319
|
+
- Playground Web: `../../apps/playground-web/README.md`
|
|
320
|
+
- Playground API: `../../apps/playground-api/README.md`
|
package/README.es.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# @navai/voice-frontend
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
|
|
5
|
+
<a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
Paquete frontend para construir agentes de voz Navai en aplicaciones web.
|
|
9
|
+
|
|
10
|
+
El objetivo es evitar boilerplate repetido para:
|
|
11
|
+
|
|
12
|
+
1. Solicitud de `client_secret` realtime.
|
|
13
|
+
2. Tools de navegacion basadas en rutas permitidas.
|
|
14
|
+
3. Carga dinamica de funciones locales.
|
|
15
|
+
4. Puente opcional con funciones backend.
|
|
16
|
+
5. Ciclo de vida React para conectar/desconectar sesion.
|
|
17
|
+
|
|
18
|
+
## Instalacion
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @navai/voice-frontend @openai/agents zod
|
|
22
|
+
npm install react
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Arquitectura del Paquete
|
|
26
|
+
|
|
27
|
+
El paquete esta separado por responsabilidades:
|
|
28
|
+
|
|
29
|
+
1. `src/backend.ts`
|
|
30
|
+
Cliente HTTP para rutas backend:
|
|
31
|
+
- `POST /navai/realtime/client-secret`
|
|
32
|
+
- `GET /navai/functions`
|
|
33
|
+
- `POST /navai/functions/execute`
|
|
34
|
+
|
|
35
|
+
2. `src/runtime.ts`
|
|
36
|
+
Resolver de runtime para:
|
|
37
|
+
- seleccion de modulo de rutas
|
|
38
|
+
- filtrado de modulos de funciones por `NAVAI_FUNCTIONS_FOLDERS`
|
|
39
|
+
- override opcional de modelo
|
|
40
|
+
|
|
41
|
+
3. `src/functions.ts`
|
|
42
|
+
Loader de funciones locales:
|
|
43
|
+
- importa modulos desde loaders generados
|
|
44
|
+
- transforma exports en definiciones de tools normalizadas y ejecutables
|
|
45
|
+
|
|
46
|
+
4. `src/agent.ts`
|
|
47
|
+
Builder del agente:
|
|
48
|
+
- crea `RealtimeAgent`
|
|
49
|
+
- inyecta tools base (`navigate_to`, `execute_app_function`)
|
|
50
|
+
- agrega aliases directos por funcion permitida cuando aplica
|
|
51
|
+
|
|
52
|
+
5. `src/useWebVoiceAgent.ts`
|
|
53
|
+
Wrapper de ciclo de vida React:
|
|
54
|
+
- construye runtime config
|
|
55
|
+
- solicita client secret
|
|
56
|
+
- descubre funciones backend
|
|
57
|
+
- construye el agente
|
|
58
|
+
- abre/cierra `RealtimeSession`
|
|
59
|
+
|
|
60
|
+
6. `src/routes.ts`
|
|
61
|
+
Helpers para resolver texto natural hacia rutas permitidas.
|
|
62
|
+
|
|
63
|
+
## Flujo Runtime de Punta a Punta
|
|
64
|
+
|
|
65
|
+
Flujo del hook (`useWebVoiceAgent`):
|
|
66
|
+
|
|
67
|
+
1. Resuelve runtime config desde `moduleLoaders` + `defaultRoutes` + env/opciones.
|
|
68
|
+
2. Crea backend client con `apiBaseUrl` o `NAVAI_API_URL`.
|
|
69
|
+
3. En `start()`:
|
|
70
|
+
- solicita client secret.
|
|
71
|
+
- solicita listado de funciones backend.
|
|
72
|
+
- construye agente Navai con funciones locales + backend.
|
|
73
|
+
- conecta `RealtimeSession`.
|
|
74
|
+
4. En `stop()`:
|
|
75
|
+
- cierra sesion y resetea estado.
|
|
76
|
+
|
|
77
|
+
Maquina de estados expuesta por el hook:
|
|
78
|
+
|
|
79
|
+
- `idle`
|
|
80
|
+
- `connecting`
|
|
81
|
+
- `connected`
|
|
82
|
+
- `error`
|
|
83
|
+
|
|
84
|
+
Estado de voz del agente expuesto por el hook:
|
|
85
|
+
|
|
86
|
+
- `agentVoiceState`: `idle | speaking`
|
|
87
|
+
- `isAgentSpeaking`: `boolean`
|
|
88
|
+
|
|
89
|
+
`agentVoiceState` se actualiza con los eventos realtime `audio_start`, `audio_stopped` y `audio_interrupted`.
|
|
90
|
+
|
|
91
|
+
## API Publica
|
|
92
|
+
|
|
93
|
+
Exports principales:
|
|
94
|
+
|
|
95
|
+
- `buildNavaiAgent(...)`
|
|
96
|
+
- `createNavaiBackendClient(...)`
|
|
97
|
+
- `resolveNavaiFrontendRuntimeConfig(...)`
|
|
98
|
+
- `loadNavaiFunctions(...)`
|
|
99
|
+
- `useWebVoiceAgent(...)`
|
|
100
|
+
- `resolveNavaiRoute(...)`
|
|
101
|
+
- `getNavaiRoutePromptLines(...)`
|
|
102
|
+
|
|
103
|
+
Tipos utiles:
|
|
104
|
+
|
|
105
|
+
- `NavaiRoute`
|
|
106
|
+
- `NavaiFunctionDefinition`
|
|
107
|
+
- `NavaiFunctionsRegistry`
|
|
108
|
+
- `NavaiBackendFunctionDefinition`
|
|
109
|
+
- `UseWebVoiceAgentOptions`
|
|
110
|
+
|
|
111
|
+
## Modelo de Tools y Comportamiento
|
|
112
|
+
|
|
113
|
+
`buildNavaiAgent` siempre registra:
|
|
114
|
+
|
|
115
|
+
- `navigate_to`
|
|
116
|
+
- `execute_app_function`
|
|
117
|
+
|
|
118
|
+
Aliases directos opcionales:
|
|
119
|
+
|
|
120
|
+
- para cada funcion permitida se puede crear una tool directa.
|
|
121
|
+
- nombres reservados nunca se exponen como tool directa (`navigate_to`, `execute_app_function`).
|
|
122
|
+
- ids invalidos de tool se omiten (la funcion sigue accesible via `execute_app_function`).
|
|
123
|
+
|
|
124
|
+
Precedencia de ejecucion:
|
|
125
|
+
|
|
126
|
+
1. Intenta funcion local/frontend.
|
|
127
|
+
2. Si no existe, intenta funcion backend.
|
|
128
|
+
3. Si ambas tienen el mismo nombre, gana frontend y backend se ignora con warning.
|
|
129
|
+
|
|
130
|
+
## Internos de Carga Dinamica de Funciones
|
|
131
|
+
|
|
132
|
+
`loadNavaiFunctions` soporta estos formatos de export:
|
|
133
|
+
|
|
134
|
+
1. Funcion exportada.
|
|
135
|
+
2. Clase exportada (metodos de instancia se vuelven funciones).
|
|
136
|
+
3. Objeto exportado (miembros callables se vuelven funciones).
|
|
137
|
+
|
|
138
|
+
Reglas de normalizacion de nombre:
|
|
139
|
+
|
|
140
|
+
- snake_case en minusculas.
|
|
141
|
+
- elimina caracteres invalidos.
|
|
142
|
+
- colisiones se renombran con sufijos (`_2`, `_3`, ...).
|
|
143
|
+
|
|
144
|
+
Reglas de mapeo de argumentos:
|
|
145
|
+
|
|
146
|
+
- usa `payload.args` o `payload.arguments` como argumentos directos.
|
|
147
|
+
- si no, usa `payload.value` como primer argumento.
|
|
148
|
+
- si no, usa payload completo como primer argumento.
|
|
149
|
+
- agrega contexto cuando la aridad indica un argumento adicional.
|
|
150
|
+
|
|
151
|
+
Para metodos de clase:
|
|
152
|
+
|
|
153
|
+
- args de constructor: `payload.constructorArgs`.
|
|
154
|
+
- args del metodo: `payload.methodArgs`.
|
|
155
|
+
|
|
156
|
+
## Resolucion Runtime y Precedencia de Env
|
|
157
|
+
|
|
158
|
+
Prioridad de entrada en `resolveNavaiFrontendRuntimeConfig`:
|
|
159
|
+
|
|
160
|
+
1. Argumentos explicitos de funcion.
|
|
161
|
+
2. Claves del objeto env.
|
|
162
|
+
3. Defaults del paquete.
|
|
163
|
+
|
|
164
|
+
Claves usadas:
|
|
165
|
+
|
|
166
|
+
- `NAVAI_ROUTES_FILE`
|
|
167
|
+
- `NAVAI_FUNCTIONS_FOLDERS`
|
|
168
|
+
- `NAVAI_REALTIME_MODEL`
|
|
169
|
+
|
|
170
|
+
Defaults:
|
|
171
|
+
|
|
172
|
+
- archivo de rutas: `src/ai/routes.ts`
|
|
173
|
+
- carpeta de funciones: `src/ai/functions-modules`
|
|
174
|
+
|
|
175
|
+
Formatos aceptados en matcher de rutas:
|
|
176
|
+
|
|
177
|
+
- carpeta: `src/ai/functions-modules`
|
|
178
|
+
- recursivo: `src/ai/functions-modules/...`
|
|
179
|
+
- wildcard: `src/features/*/voice-functions`
|
|
180
|
+
- archivo explicito: `src/ai/functions-modules/secret.ts`
|
|
181
|
+
- CSV: `a,b,c`
|
|
182
|
+
|
|
183
|
+
Comportamiento fallback:
|
|
184
|
+
|
|
185
|
+
- si `NAVAI_FUNCTIONS_FOLDERS` no matchea modulos, emite warning.
|
|
186
|
+
- hace fallback a carpeta de funciones por defecto.
|
|
187
|
+
|
|
188
|
+
## Comportamiento del Backend Client
|
|
189
|
+
|
|
190
|
+
Prioridad de base URL en `createNavaiBackendClient`:
|
|
191
|
+
|
|
192
|
+
1. Opcion `apiBaseUrl`.
|
|
193
|
+
2. `env.NAVAI_API_URL`.
|
|
194
|
+
3. Fallback `http://localhost:3000`.
|
|
195
|
+
|
|
196
|
+
Metodos:
|
|
197
|
+
|
|
198
|
+
- `createClientSecret(input?)`
|
|
199
|
+
- `listFunctions()`
|
|
200
|
+
- `executeFunction({ functionName, payload })`
|
|
201
|
+
|
|
202
|
+
Manejo de errores:
|
|
203
|
+
|
|
204
|
+
- fallos de red/HTTP lanzan error en create/execute.
|
|
205
|
+
- el listado de funciones retorna warnings + lista vacia en fallos.
|
|
206
|
+
|
|
207
|
+
## CLI Generador de Module Loaders
|
|
208
|
+
|
|
209
|
+
Este paquete incluye:
|
|
210
|
+
|
|
211
|
+
- `navai-generate-web-loaders`
|
|
212
|
+
|
|
213
|
+
Comportamiento por defecto:
|
|
214
|
+
|
|
215
|
+
1. Lee `.env` y env del proceso.
|
|
216
|
+
2. Resuelve `NAVAI_FUNCTIONS_FOLDERS` y `NAVAI_ROUTES_FILE`.
|
|
217
|
+
3. Selecciona modulos solo en rutas de funciones configuradas.
|
|
218
|
+
4. Incluye modulo de rutas configurado cuando difiere del modulo por defecto.
|
|
219
|
+
5. Escribe `src/ai/generated-module-loaders.ts`.
|
|
220
|
+
|
|
221
|
+
Uso manual:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
navai-generate-web-loaders
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Flags utiles:
|
|
228
|
+
|
|
229
|
+
- `--project-root <path>`
|
|
230
|
+
- `--src-root <path>`
|
|
231
|
+
- `--output-file <path>`
|
|
232
|
+
- `--env-file <path>`
|
|
233
|
+
- `--default-functions-folder <path>`
|
|
234
|
+
- `--default-routes-file <path>`
|
|
235
|
+
- `--type-import <module>`
|
|
236
|
+
- `--export-name <identifier>`
|
|
237
|
+
|
|
238
|
+
## Auto Configuracion al Instalar desde npm
|
|
239
|
+
|
|
240
|
+
El `postinstall` puede agregar scripts faltantes en el consumidor:
|
|
241
|
+
|
|
242
|
+
- `generate:module-loaders` -> `navai-generate-web-loaders`
|
|
243
|
+
- `predev` -> `npm run generate:module-loaders`
|
|
244
|
+
- `prebuild` -> `npm run generate:module-loaders`
|
|
245
|
+
- `pretypecheck` -> `npm run generate:module-loaders`
|
|
246
|
+
- `prelint` -> `npm run generate:module-loaders`
|
|
247
|
+
|
|
248
|
+
Reglas:
|
|
249
|
+
|
|
250
|
+
- solo agrega scripts faltantes.
|
|
251
|
+
- nunca sobreescribe scripts existentes.
|
|
252
|
+
|
|
253
|
+
Desactivar auto setup:
|
|
254
|
+
|
|
255
|
+
- `NAVAI_SKIP_AUTO_SETUP=1`
|
|
256
|
+
- o `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
|
|
257
|
+
|
|
258
|
+
Ejecutor manual de setup:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx navai-setup-voice-frontend
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Ejemplos de Integracion
|
|
265
|
+
|
|
266
|
+
Integracion imperativa:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { RealtimeSession } from "@openai/agents/realtime";
|
|
270
|
+
import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
|
|
271
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
272
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
273
|
+
|
|
274
|
+
const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
|
|
275
|
+
const secret = await backend.createClientSecret();
|
|
276
|
+
const backendList = await backend.listFunctions();
|
|
277
|
+
|
|
278
|
+
const { agent, warnings } = await buildNavaiAgent({
|
|
279
|
+
navigate: (path) => router.navigate(path),
|
|
280
|
+
routes: NAVAI_ROUTE_ITEMS,
|
|
281
|
+
functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
282
|
+
backendFunctions: backendList.functions,
|
|
283
|
+
executeBackendFunction: backend.executeFunction
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
warnings.forEach((w) => console.warn(w));
|
|
287
|
+
|
|
288
|
+
const session = new RealtimeSession(agent);
|
|
289
|
+
await session.connect({ apiKey: secret.value });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Integracion con hook React:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { useWebVoiceAgent } from "@navai/voice-frontend";
|
|
296
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
297
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
298
|
+
|
|
299
|
+
const voice = useWebVoiceAgent({
|
|
300
|
+
navigate: (path) => router.navigate(path),
|
|
301
|
+
moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
302
|
+
defaultRoutes: NAVAI_ROUTE_ITEMS,
|
|
303
|
+
env: import.meta.env as Record<string, string | undefined>
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Notas Operativas
|
|
308
|
+
|
|
309
|
+
- los warnings se emiten con `console.warn` desde runtime, backend list y agent builder.
|
|
310
|
+
- una funcion desconocida retorna payload estructurado `ok: false`.
|
|
311
|
+
- si el modulo de rutas falla o tiene shape invalido, el resolver hace fallback a rutas por defecto.
|
|
312
|
+
|
|
313
|
+
## Documentacion Relacionada
|
|
314
|
+
|
|
315
|
+
- Version en espanol: `README.es.md`
|
|
316
|
+
- Version en ingles: `README.en.md`
|
|
317
|
+
- Paquete backend: `../voice-backend/README.md`
|
|
318
|
+
- Paquete mobile: `../voice-mobile/README.md`
|
|
319
|
+
- Playground Web: `../../apps/playground-web/README.md`
|
|
320
|
+
- Playground API: `../../apps/playground-api/README.md`
|
package/README.md
CHANGED
|
@@ -1,126 +1,320 @@
|
|
|
1
|
-
# @navai/voice-frontend
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
# @navai/voice-frontend
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
|
|
5
|
+
<a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
Frontend package to build Navai voice agents in web applications.
|
|
9
|
+
|
|
10
|
+
It removes repeated boilerplate for:
|
|
11
|
+
|
|
12
|
+
1. Realtime client secret requests.
|
|
13
|
+
2. Route-aware navigation tools.
|
|
14
|
+
3. Dynamic local function loading.
|
|
15
|
+
4. Optional backend function bridging.
|
|
16
|
+
5. React hook lifecycle for connect/disconnect.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @navai/voice-frontend @openai/agents zod
|
|
22
|
+
npm install react
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Package Architecture
|
|
26
|
+
|
|
27
|
+
This package is intentionally split by concern:
|
|
28
|
+
|
|
29
|
+
1. `src/backend.ts`
|
|
30
|
+
HTTP client for backend routes:
|
|
31
|
+
- `POST /navai/realtime/client-secret`
|
|
32
|
+
- `GET /navai/functions`
|
|
33
|
+
- `POST /navai/functions/execute`
|
|
34
|
+
|
|
35
|
+
2. `src/runtime.ts`
|
|
36
|
+
Runtime resolver for:
|
|
37
|
+
- route module selection
|
|
38
|
+
- function module filtering by `NAVAI_FUNCTIONS_FOLDERS`
|
|
39
|
+
- optional model override
|
|
40
|
+
|
|
41
|
+
3. `src/functions.ts`
|
|
42
|
+
Local function loader:
|
|
43
|
+
- imports modules from generated loaders
|
|
44
|
+
- converts exports into normalized callable tool definitions
|
|
45
|
+
|
|
46
|
+
4. `src/agent.ts`
|
|
47
|
+
Agent builder:
|
|
48
|
+
- creates `RealtimeAgent`
|
|
49
|
+
- injects built-in tools (`navigate_to`, `execute_app_function`)
|
|
50
|
+
- optionally adds direct alias tools for each allowed function
|
|
51
|
+
|
|
52
|
+
5. `src/useWebVoiceAgent.ts`
|
|
53
|
+
React lifecycle wrapper:
|
|
54
|
+
- builds runtime config
|
|
55
|
+
- requests client secret
|
|
56
|
+
- discovers backend functions
|
|
57
|
+
- builds agent
|
|
58
|
+
- opens/closes `RealtimeSession`
|
|
59
|
+
|
|
60
|
+
6. `src/routes.ts`
|
|
61
|
+
Route matching helpers for natural language to path resolution.
|
|
62
|
+
|
|
63
|
+
## End-to-End Runtime Flow
|
|
64
|
+
|
|
65
|
+
Hook-driven runtime flow (`useWebVoiceAgent`):
|
|
66
|
+
|
|
67
|
+
1. Resolve runtime config from `moduleLoaders` + `defaultRoutes` + env/options.
|
|
68
|
+
2. Create backend client with `apiBaseUrl` or `NAVAI_API_URL`.
|
|
69
|
+
3. On `start()`:
|
|
70
|
+
- request client secret.
|
|
71
|
+
- fetch backend function list.
|
|
72
|
+
- build Navai agent with local + backend functions.
|
|
73
|
+
- connect `RealtimeSession`.
|
|
74
|
+
4. On `stop()`:
|
|
75
|
+
- close session and reset state.
|
|
76
|
+
|
|
77
|
+
State machine exposed by hook:
|
|
78
|
+
|
|
79
|
+
- `idle`
|
|
80
|
+
- `connecting`
|
|
81
|
+
- `connected`
|
|
82
|
+
- `error`
|
|
83
|
+
|
|
84
|
+
Agent voice state exposed by the hook:
|
|
85
|
+
|
|
86
|
+
- `agentVoiceState`: `idle | speaking`
|
|
87
|
+
- `isAgentSpeaking`: `boolean`
|
|
88
|
+
|
|
89
|
+
`agentVoiceState` is driven by realtime events `audio_start`, `audio_stopped`, and `audio_interrupted`.
|
|
90
|
+
|
|
91
|
+
## Public API
|
|
92
|
+
|
|
93
|
+
Main exports:
|
|
94
|
+
|
|
95
|
+
- `buildNavaiAgent(...)`
|
|
96
|
+
- `createNavaiBackendClient(...)`
|
|
97
|
+
- `resolveNavaiFrontendRuntimeConfig(...)`
|
|
98
|
+
- `loadNavaiFunctions(...)`
|
|
99
|
+
- `useWebVoiceAgent(...)`
|
|
100
|
+
- `resolveNavaiRoute(...)`
|
|
101
|
+
- `getNavaiRoutePromptLines(...)`
|
|
102
|
+
|
|
103
|
+
Useful types:
|
|
104
|
+
|
|
105
|
+
- `NavaiRoute`
|
|
106
|
+
- `NavaiFunctionDefinition`
|
|
107
|
+
- `NavaiFunctionsRegistry`
|
|
108
|
+
- `NavaiBackendFunctionDefinition`
|
|
109
|
+
- `UseWebVoiceAgentOptions`
|
|
110
|
+
|
|
111
|
+
## Tool Model and Behavior
|
|
112
|
+
|
|
113
|
+
`buildNavaiAgent` always registers:
|
|
114
|
+
|
|
115
|
+
- `navigate_to`
|
|
116
|
+
- `execute_app_function`
|
|
117
|
+
|
|
118
|
+
Optional direct alias tools:
|
|
119
|
+
|
|
120
|
+
- for each allowed function name, a direct tool can be created.
|
|
121
|
+
- reserved names are never used as direct tools (`navigate_to`, `execute_app_function`).
|
|
122
|
+
- invalid tool ids are skipped (kept accessible via `execute_app_function`).
|
|
123
|
+
|
|
124
|
+
Execution precedence:
|
|
125
|
+
|
|
126
|
+
1. Try frontend/local function first.
|
|
127
|
+
2. If missing, try backend function.
|
|
128
|
+
3. If both exist with same name, frontend wins and backend is ignored with warning.
|
|
129
|
+
|
|
130
|
+
## Dynamic Function Loading Internals
|
|
131
|
+
|
|
132
|
+
`loadNavaiFunctions` supports module export shapes:
|
|
133
|
+
|
|
134
|
+
1. Exported function.
|
|
135
|
+
2. Exported class (instance methods become functions).
|
|
136
|
+
3. Exported object (callable members become functions).
|
|
137
|
+
|
|
138
|
+
Name normalization rules:
|
|
139
|
+
|
|
140
|
+
- snake_case lowercase.
|
|
141
|
+
- invalid chars removed.
|
|
142
|
+
- collisions are renamed with suffixes (`_2`, `_3`, ...).
|
|
143
|
+
|
|
144
|
+
Argument mapping rules:
|
|
145
|
+
|
|
146
|
+
- `payload.args` or `payload.arguments` as direct args.
|
|
147
|
+
- else `payload.value` as first arg.
|
|
148
|
+
- else full payload as first arg.
|
|
149
|
+
- context appended when arity indicates one more argument.
|
|
150
|
+
|
|
151
|
+
For class methods:
|
|
152
|
+
|
|
153
|
+
- constructor args: `payload.constructorArgs`.
|
|
154
|
+
- method args: `payload.methodArgs`.
|
|
155
|
+
|
|
156
|
+
## Runtime Resolution and Env Precedence
|
|
157
|
+
|
|
158
|
+
`resolveNavaiFrontendRuntimeConfig` input priority:
|
|
159
|
+
|
|
160
|
+
1. Explicit function args.
|
|
161
|
+
2. Env object keys.
|
|
162
|
+
3. Package defaults.
|
|
163
|
+
|
|
164
|
+
Keys used:
|
|
165
|
+
|
|
166
|
+
- `NAVAI_ROUTES_FILE`
|
|
167
|
+
- `NAVAI_FUNCTIONS_FOLDERS`
|
|
168
|
+
- `NAVAI_REALTIME_MODEL`
|
|
169
|
+
|
|
170
|
+
Defaults:
|
|
171
|
+
|
|
172
|
+
- routes file: `src/ai/routes.ts`
|
|
173
|
+
- functions folder: `src/ai/functions-modules`
|
|
174
|
+
|
|
175
|
+
Path matcher formats:
|
|
176
|
+
|
|
177
|
+
- folder: `src/ai/functions-modules`
|
|
178
|
+
- recursive: `src/ai/functions-modules/...`
|
|
179
|
+
- wildcard: `src/features/*/voice-functions`
|
|
180
|
+
- explicit file: `src/ai/functions-modules/secret.ts`
|
|
181
|
+
- CSV list: `a,b,c`
|
|
182
|
+
|
|
183
|
+
Fallback behavior:
|
|
184
|
+
|
|
185
|
+
- if configured folders match no modules, warning is emitted.
|
|
186
|
+
- resolver falls back to default functions folder.
|
|
187
|
+
|
|
188
|
+
## Backend Client Behavior
|
|
189
|
+
|
|
190
|
+
`createNavaiBackendClient` base URL priority:
|
|
191
|
+
|
|
192
|
+
1. `apiBaseUrl` option.
|
|
193
|
+
2. `env.NAVAI_API_URL`.
|
|
194
|
+
3. fallback `http://localhost:3000`.
|
|
195
|
+
|
|
196
|
+
Methods:
|
|
197
|
+
|
|
198
|
+
- `createClientSecret(input?)`
|
|
199
|
+
- `listFunctions()`
|
|
200
|
+
- `executeFunction({ functionName, payload })`
|
|
201
|
+
|
|
202
|
+
Error handling:
|
|
203
|
+
|
|
204
|
+
- network/HTTP failures throw for create/execute.
|
|
205
|
+
- function listing returns warnings and empty list on failures.
|
|
206
|
+
|
|
207
|
+
## Generated Module Loader CLI
|
|
208
|
+
|
|
209
|
+
This package ships:
|
|
210
|
+
|
|
211
|
+
- `navai-generate-web-loaders`
|
|
212
|
+
|
|
213
|
+
Default command behavior:
|
|
214
|
+
|
|
215
|
+
1. Reads `.env` and process env.
|
|
216
|
+
2. Resolves `NAVAI_FUNCTIONS_FOLDERS` and `NAVAI_ROUTES_FILE`.
|
|
217
|
+
3. Selects modules only from configured function paths.
|
|
218
|
+
4. Optionally includes configured route module if it differs from default route module.
|
|
219
|
+
5. Writes `src/ai/generated-module-loaders.ts`.
|
|
220
|
+
|
|
221
|
+
Manual usage:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
navai-generate-web-loaders
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Useful flags:
|
|
228
|
+
|
|
229
|
+
- `--project-root <path>`
|
|
230
|
+
- `--src-root <path>`
|
|
231
|
+
- `--output-file <path>`
|
|
232
|
+
- `--env-file <path>`
|
|
233
|
+
- `--default-functions-folder <path>`
|
|
234
|
+
- `--default-routes-file <path>`
|
|
235
|
+
- `--type-import <module>`
|
|
236
|
+
- `--export-name <identifier>`
|
|
237
|
+
|
|
238
|
+
## Auto Setup on npm Install
|
|
239
|
+
|
|
240
|
+
Postinstall script can auto-add missing scripts in consumer app:
|
|
241
|
+
|
|
242
|
+
- `generate:module-loaders` -> `navai-generate-web-loaders`
|
|
243
|
+
- `predev` -> `npm run generate:module-loaders`
|
|
244
|
+
- `prebuild` -> `npm run generate:module-loaders`
|
|
245
|
+
- `pretypecheck` -> `npm run generate:module-loaders`
|
|
246
|
+
- `prelint` -> `npm run generate:module-loaders`
|
|
247
|
+
|
|
248
|
+
Rules:
|
|
249
|
+
|
|
250
|
+
- only missing scripts are added.
|
|
251
|
+
- existing scripts are never overwritten.
|
|
252
|
+
|
|
253
|
+
Disable auto setup:
|
|
254
|
+
|
|
255
|
+
- `NAVAI_SKIP_AUTO_SETUP=1`
|
|
256
|
+
- or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
|
|
257
|
+
|
|
258
|
+
Manual setup runner:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx navai-setup-voice-frontend
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Integration Examples
|
|
265
|
+
|
|
266
|
+
Imperative integration:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { RealtimeSession } from "@openai/agents/realtime";
|
|
270
|
+
import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
|
|
271
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
272
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
273
|
+
|
|
274
|
+
const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
|
|
275
|
+
const secret = await backend.createClientSecret();
|
|
276
|
+
const backendList = await backend.listFunctions();
|
|
277
|
+
|
|
278
|
+
const { agent, warnings } = await buildNavaiAgent({
|
|
279
|
+
navigate: (path) => router.navigate(path),
|
|
280
|
+
routes: NAVAI_ROUTE_ITEMS,
|
|
281
|
+
functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
282
|
+
backendFunctions: backendList.functions,
|
|
283
|
+
executeBackendFunction: backend.executeFunction
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
warnings.forEach((w) => console.warn(w));
|
|
287
|
+
|
|
288
|
+
const session = new RealtimeSession(agent);
|
|
289
|
+
await session.connect({ apiKey: secret.value });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
React hook integration:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { useWebVoiceAgent } from "@navai/voice-frontend";
|
|
296
|
+
import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
|
|
297
|
+
import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
|
|
298
|
+
|
|
299
|
+
const voice = useWebVoiceAgent({
|
|
300
|
+
navigate: (path) => router.navigate(path),
|
|
301
|
+
moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
|
|
302
|
+
defaultRoutes: NAVAI_ROUTE_ITEMS,
|
|
303
|
+
env: import.meta.env as Record<string, string | undefined>
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Operational Notes
|
|
308
|
+
|
|
309
|
+
- warnings are emitted with `console.warn` from runtime, backend list, and agent builder.
|
|
310
|
+
- unknown function execution returns structured `ok: false` payload.
|
|
311
|
+
- if route module fails to load or has invalid shape, resolver falls back to default routes.
|
|
312
|
+
|
|
313
|
+
## Related Docs
|
|
314
|
+
|
|
315
|
+
- Spanish version: `README.es.md`
|
|
316
|
+
- English version: `README.en.md`
|
|
317
|
+
- Backend package: `../voice-backend/README.md`
|
|
318
|
+
- Mobile package: `../voice-mobile/README.md`
|
|
319
|
+
- Playground Web: `../../apps/playground-web/README.md`
|
|
320
|
+
- Playground API: `../../apps/playground-api/README.md`
|
package/dist/index.cjs
CHANGED
|
@@ -760,6 +760,7 @@ function emitWarnings(warnings) {
|
|
|
760
760
|
}
|
|
761
761
|
function useWebVoiceAgent(options) {
|
|
762
762
|
const sessionRef = (0, import_react.useRef)(null);
|
|
763
|
+
const attachedRealtimeSessionRef = (0, import_react.useRef)(null);
|
|
763
764
|
const runtimeConfigPromise = (0, import_react.useMemo)(
|
|
764
765
|
() => resolveNavaiFrontendRuntimeConfig({
|
|
765
766
|
moduleLoaders: options.moduleLoaders,
|
|
@@ -790,15 +791,61 @@ function useWebVoiceAgent(options) {
|
|
|
790
791
|
[options.apiBaseUrl, options.env]
|
|
791
792
|
);
|
|
792
793
|
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
794
|
+
const [agentVoiceState, setAgentVoiceState] = (0, import_react.useState)("idle");
|
|
793
795
|
const [error, setError] = (0, import_react.useState)(null);
|
|
796
|
+
const setAgentVoiceStateIfChanged = (0, import_react.useCallback)((next) => {
|
|
797
|
+
setAgentVoiceState((current) => current === next ? current : next);
|
|
798
|
+
}, []);
|
|
799
|
+
const handleSessionAudioStart = (0, import_react.useCallback)(() => {
|
|
800
|
+
setAgentVoiceStateIfChanged("speaking");
|
|
801
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
802
|
+
const handleSessionAudioStopped = (0, import_react.useCallback)(() => {
|
|
803
|
+
setAgentVoiceStateIfChanged("idle");
|
|
804
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
805
|
+
const handleSessionAudioInterrupted = (0, import_react.useCallback)(() => {
|
|
806
|
+
setAgentVoiceStateIfChanged("idle");
|
|
807
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
808
|
+
const handleSessionError = (0, import_react.useCallback)(() => {
|
|
809
|
+
setAgentVoiceStateIfChanged("idle");
|
|
810
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
811
|
+
const detachSessionAudioListeners = (0, import_react.useCallback)(() => {
|
|
812
|
+
const attachedSession = attachedRealtimeSessionRef.current;
|
|
813
|
+
if (!attachedSession) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
attachedSession.off("audio_start", handleSessionAudioStart);
|
|
817
|
+
attachedSession.off("audio_stopped", handleSessionAudioStopped);
|
|
818
|
+
attachedSession.off("audio_interrupted", handleSessionAudioInterrupted);
|
|
819
|
+
attachedSession.off("error", handleSessionError);
|
|
820
|
+
attachedRealtimeSessionRef.current = null;
|
|
821
|
+
}, [handleSessionAudioInterrupted, handleSessionAudioStart, handleSessionAudioStopped, handleSessionError]);
|
|
822
|
+
const attachSessionAudioListeners = (0, import_react.useCallback)(
|
|
823
|
+
(session) => {
|
|
824
|
+
detachSessionAudioListeners();
|
|
825
|
+
session.on("audio_start", handleSessionAudioStart);
|
|
826
|
+
session.on("audio_stopped", handleSessionAudioStopped);
|
|
827
|
+
session.on("audio_interrupted", handleSessionAudioInterrupted);
|
|
828
|
+
session.on("error", handleSessionError);
|
|
829
|
+
attachedRealtimeSessionRef.current = session;
|
|
830
|
+
},
|
|
831
|
+
[
|
|
832
|
+
detachSessionAudioListeners,
|
|
833
|
+
handleSessionAudioInterrupted,
|
|
834
|
+
handleSessionAudioStart,
|
|
835
|
+
handleSessionAudioStopped,
|
|
836
|
+
handleSessionError
|
|
837
|
+
]
|
|
838
|
+
);
|
|
794
839
|
const stop = (0, import_react.useCallback)(() => {
|
|
840
|
+
detachSessionAudioListeners();
|
|
795
841
|
try {
|
|
796
842
|
sessionRef.current?.close();
|
|
797
843
|
} finally {
|
|
798
844
|
sessionRef.current = null;
|
|
799
845
|
setStatus("idle");
|
|
846
|
+
setAgentVoiceStateIfChanged("idle");
|
|
800
847
|
}
|
|
801
|
-
}, []);
|
|
848
|
+
}, [detachSessionAudioListeners, setAgentVoiceStateIfChanged]);
|
|
802
849
|
(0, import_react.useEffect)(() => {
|
|
803
850
|
return () => {
|
|
804
851
|
stop();
|
|
@@ -810,6 +857,7 @@ function useWebVoiceAgent(options) {
|
|
|
810
857
|
}
|
|
811
858
|
setError(null);
|
|
812
859
|
setStatus("connecting");
|
|
860
|
+
setAgentVoiceStateIfChanged("idle");
|
|
813
861
|
try {
|
|
814
862
|
const runtimeConfig = await runtimeConfigPromise;
|
|
815
863
|
const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
|
|
@@ -824,6 +872,7 @@ function useWebVoiceAgent(options) {
|
|
|
824
872
|
});
|
|
825
873
|
emitWarnings([...runtimeConfig.warnings, ...backendFunctionsResult.warnings, ...warnings]);
|
|
826
874
|
const session = new import_realtime2.RealtimeSession(agent);
|
|
875
|
+
attachSessionAudioListeners(session);
|
|
827
876
|
if (runtimeConfig.modelOverride) {
|
|
828
877
|
await session.connect({ apiKey: secretPayload.value, model: runtimeConfig.modelOverride });
|
|
829
878
|
} else {
|
|
@@ -835,18 +884,30 @@ function useWebVoiceAgent(options) {
|
|
|
835
884
|
const message = formatError(startError);
|
|
836
885
|
setError(message);
|
|
837
886
|
setStatus("error");
|
|
887
|
+
setAgentVoiceStateIfChanged("idle");
|
|
888
|
+
detachSessionAudioListeners();
|
|
838
889
|
try {
|
|
839
890
|
sessionRef.current?.close();
|
|
840
891
|
} catch {
|
|
841
892
|
}
|
|
842
893
|
sessionRef.current = null;
|
|
843
894
|
}
|
|
844
|
-
}, [
|
|
895
|
+
}, [
|
|
896
|
+
attachSessionAudioListeners,
|
|
897
|
+
backendClient,
|
|
898
|
+
detachSessionAudioListeners,
|
|
899
|
+
options.navigate,
|
|
900
|
+
runtimeConfigPromise,
|
|
901
|
+
setAgentVoiceStateIfChanged,
|
|
902
|
+
status
|
|
903
|
+
]);
|
|
845
904
|
return {
|
|
846
905
|
status,
|
|
906
|
+
agentVoiceState,
|
|
847
907
|
error,
|
|
848
908
|
isConnecting: status === "connecting",
|
|
849
909
|
isConnected: status === "connected",
|
|
910
|
+
isAgentSpeaking: agentVoiceState === "speaking",
|
|
850
911
|
start,
|
|
851
912
|
stop
|
|
852
913
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -104,6 +104,7 @@ type ResolveNavaiFrontendRuntimeConfigResult = {
|
|
|
104
104
|
declare function resolveNavaiFrontendRuntimeConfig(options: ResolveNavaiFrontendRuntimeConfigOptions): Promise<ResolveNavaiFrontendRuntimeConfigResult>;
|
|
105
105
|
|
|
106
106
|
type VoiceStatus = "idle" | "connecting" | "connected" | "error";
|
|
107
|
+
type AgentVoiceState = "idle" | "speaking";
|
|
107
108
|
type NavaiFrontendEnv = Record<string, string | undefined>;
|
|
108
109
|
type UseWebVoiceAgentOptions = {
|
|
109
110
|
navigate: (path: string) => void;
|
|
@@ -119,9 +120,11 @@ type UseWebVoiceAgentOptions = {
|
|
|
119
120
|
};
|
|
120
121
|
type UseWebVoiceAgentResult = {
|
|
121
122
|
status: VoiceStatus;
|
|
123
|
+
agentVoiceState: AgentVoiceState;
|
|
122
124
|
error: string | null;
|
|
123
125
|
isConnecting: boolean;
|
|
124
126
|
isConnected: boolean;
|
|
127
|
+
isAgentSpeaking: boolean;
|
|
125
128
|
start: () => Promise<void>;
|
|
126
129
|
stop: () => void;
|
|
127
130
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -104,6 +104,7 @@ type ResolveNavaiFrontendRuntimeConfigResult = {
|
|
|
104
104
|
declare function resolveNavaiFrontendRuntimeConfig(options: ResolveNavaiFrontendRuntimeConfigOptions): Promise<ResolveNavaiFrontendRuntimeConfigResult>;
|
|
105
105
|
|
|
106
106
|
type VoiceStatus = "idle" | "connecting" | "connected" | "error";
|
|
107
|
+
type AgentVoiceState = "idle" | "speaking";
|
|
107
108
|
type NavaiFrontendEnv = Record<string, string | undefined>;
|
|
108
109
|
type UseWebVoiceAgentOptions = {
|
|
109
110
|
navigate: (path: string) => void;
|
|
@@ -119,9 +120,11 @@ type UseWebVoiceAgentOptions = {
|
|
|
119
120
|
};
|
|
120
121
|
type UseWebVoiceAgentResult = {
|
|
121
122
|
status: VoiceStatus;
|
|
123
|
+
agentVoiceState: AgentVoiceState;
|
|
122
124
|
error: string | null;
|
|
123
125
|
isConnecting: boolean;
|
|
124
126
|
isConnected: boolean;
|
|
127
|
+
isAgentSpeaking: boolean;
|
|
125
128
|
start: () => Promise<void>;
|
|
126
129
|
stop: () => void;
|
|
127
130
|
};
|
package/dist/index.js
CHANGED
|
@@ -728,6 +728,7 @@ function emitWarnings(warnings) {
|
|
|
728
728
|
}
|
|
729
729
|
function useWebVoiceAgent(options) {
|
|
730
730
|
const sessionRef = useRef(null);
|
|
731
|
+
const attachedRealtimeSessionRef = useRef(null);
|
|
731
732
|
const runtimeConfigPromise = useMemo(
|
|
732
733
|
() => resolveNavaiFrontendRuntimeConfig({
|
|
733
734
|
moduleLoaders: options.moduleLoaders,
|
|
@@ -758,15 +759,61 @@ function useWebVoiceAgent(options) {
|
|
|
758
759
|
[options.apiBaseUrl, options.env]
|
|
759
760
|
);
|
|
760
761
|
const [status, setStatus] = useState("idle");
|
|
762
|
+
const [agentVoiceState, setAgentVoiceState] = useState("idle");
|
|
761
763
|
const [error, setError] = useState(null);
|
|
764
|
+
const setAgentVoiceStateIfChanged = useCallback((next) => {
|
|
765
|
+
setAgentVoiceState((current) => current === next ? current : next);
|
|
766
|
+
}, []);
|
|
767
|
+
const handleSessionAudioStart = useCallback(() => {
|
|
768
|
+
setAgentVoiceStateIfChanged("speaking");
|
|
769
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
770
|
+
const handleSessionAudioStopped = useCallback(() => {
|
|
771
|
+
setAgentVoiceStateIfChanged("idle");
|
|
772
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
773
|
+
const handleSessionAudioInterrupted = useCallback(() => {
|
|
774
|
+
setAgentVoiceStateIfChanged("idle");
|
|
775
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
776
|
+
const handleSessionError = useCallback(() => {
|
|
777
|
+
setAgentVoiceStateIfChanged("idle");
|
|
778
|
+
}, [setAgentVoiceStateIfChanged]);
|
|
779
|
+
const detachSessionAudioListeners = useCallback(() => {
|
|
780
|
+
const attachedSession = attachedRealtimeSessionRef.current;
|
|
781
|
+
if (!attachedSession) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
attachedSession.off("audio_start", handleSessionAudioStart);
|
|
785
|
+
attachedSession.off("audio_stopped", handleSessionAudioStopped);
|
|
786
|
+
attachedSession.off("audio_interrupted", handleSessionAudioInterrupted);
|
|
787
|
+
attachedSession.off("error", handleSessionError);
|
|
788
|
+
attachedRealtimeSessionRef.current = null;
|
|
789
|
+
}, [handleSessionAudioInterrupted, handleSessionAudioStart, handleSessionAudioStopped, handleSessionError]);
|
|
790
|
+
const attachSessionAudioListeners = useCallback(
|
|
791
|
+
(session) => {
|
|
792
|
+
detachSessionAudioListeners();
|
|
793
|
+
session.on("audio_start", handleSessionAudioStart);
|
|
794
|
+
session.on("audio_stopped", handleSessionAudioStopped);
|
|
795
|
+
session.on("audio_interrupted", handleSessionAudioInterrupted);
|
|
796
|
+
session.on("error", handleSessionError);
|
|
797
|
+
attachedRealtimeSessionRef.current = session;
|
|
798
|
+
},
|
|
799
|
+
[
|
|
800
|
+
detachSessionAudioListeners,
|
|
801
|
+
handleSessionAudioInterrupted,
|
|
802
|
+
handleSessionAudioStart,
|
|
803
|
+
handleSessionAudioStopped,
|
|
804
|
+
handleSessionError
|
|
805
|
+
]
|
|
806
|
+
);
|
|
762
807
|
const stop = useCallback(() => {
|
|
808
|
+
detachSessionAudioListeners();
|
|
763
809
|
try {
|
|
764
810
|
sessionRef.current?.close();
|
|
765
811
|
} finally {
|
|
766
812
|
sessionRef.current = null;
|
|
767
813
|
setStatus("idle");
|
|
814
|
+
setAgentVoiceStateIfChanged("idle");
|
|
768
815
|
}
|
|
769
|
-
}, []);
|
|
816
|
+
}, [detachSessionAudioListeners, setAgentVoiceStateIfChanged]);
|
|
770
817
|
useEffect(() => {
|
|
771
818
|
return () => {
|
|
772
819
|
stop();
|
|
@@ -778,6 +825,7 @@ function useWebVoiceAgent(options) {
|
|
|
778
825
|
}
|
|
779
826
|
setError(null);
|
|
780
827
|
setStatus("connecting");
|
|
828
|
+
setAgentVoiceStateIfChanged("idle");
|
|
781
829
|
try {
|
|
782
830
|
const runtimeConfig = await runtimeConfigPromise;
|
|
783
831
|
const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
|
|
@@ -792,6 +840,7 @@ function useWebVoiceAgent(options) {
|
|
|
792
840
|
});
|
|
793
841
|
emitWarnings([...runtimeConfig.warnings, ...backendFunctionsResult.warnings, ...warnings]);
|
|
794
842
|
const session = new RealtimeSession(agent);
|
|
843
|
+
attachSessionAudioListeners(session);
|
|
795
844
|
if (runtimeConfig.modelOverride) {
|
|
796
845
|
await session.connect({ apiKey: secretPayload.value, model: runtimeConfig.modelOverride });
|
|
797
846
|
} else {
|
|
@@ -803,18 +852,30 @@ function useWebVoiceAgent(options) {
|
|
|
803
852
|
const message = formatError(startError);
|
|
804
853
|
setError(message);
|
|
805
854
|
setStatus("error");
|
|
855
|
+
setAgentVoiceStateIfChanged("idle");
|
|
856
|
+
detachSessionAudioListeners();
|
|
806
857
|
try {
|
|
807
858
|
sessionRef.current?.close();
|
|
808
859
|
} catch {
|
|
809
860
|
}
|
|
810
861
|
sessionRef.current = null;
|
|
811
862
|
}
|
|
812
|
-
}, [
|
|
863
|
+
}, [
|
|
864
|
+
attachSessionAudioListeners,
|
|
865
|
+
backendClient,
|
|
866
|
+
detachSessionAudioListeners,
|
|
867
|
+
options.navigate,
|
|
868
|
+
runtimeConfigPromise,
|
|
869
|
+
setAgentVoiceStateIfChanged,
|
|
870
|
+
status
|
|
871
|
+
]);
|
|
813
872
|
return {
|
|
814
873
|
status,
|
|
874
|
+
agentVoiceState,
|
|
815
875
|
error,
|
|
816
876
|
isConnecting: status === "connecting",
|
|
817
877
|
isConnected: status === "connected",
|
|
878
|
+
isAgentSpeaking: agentVoiceState === "speaking",
|
|
818
879
|
start,
|
|
819
880
|
stop
|
|
820
881
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navai/voice-frontend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Frontend helpers to build OpenAI Realtime voice agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"bin",
|
|
23
|
-
"README.md"
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.es.md",
|
|
25
|
+
"README.en.md"
|
|
24
26
|
],
|
|
25
27
|
"scripts": {
|
|
26
28
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|