@jchaffin/voicekit 0.2.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.
@@ -0,0 +1,294 @@
1
+ import {
2
+ emitToolResult
3
+ } from "../chunk-T3II3DRG.mjs";
4
+ import {
5
+ EventEmitter
6
+ } from "../chunk-22WLZIXO.mjs";
7
+
8
+ // src/adapters/openai.ts
9
+ import {
10
+ RealtimeSession,
11
+ RealtimeAgent,
12
+ OpenAIRealtimeWebRTC,
13
+ tool as sdkTool
14
+ } from "@openai/agents/realtime";
15
+ function convertTool(def) {
16
+ return sdkTool({
17
+ name: def.name,
18
+ description: def.description,
19
+ parameters: {
20
+ type: "object",
21
+ properties: def.parameters.properties,
22
+ required: def.parameters.required || [],
23
+ additionalProperties: false
24
+ },
25
+ execute: async (input) => {
26
+ try {
27
+ const result = await def.execute(input);
28
+ emitToolResult(def.name, input, result);
29
+ return result;
30
+ } catch (error) {
31
+ const errorResult = { success: false, error: String(error) };
32
+ emitToolResult(def.name, input, errorResult);
33
+ return errorResult;
34
+ }
35
+ }
36
+ });
37
+ }
38
+ var OpenAISession = class extends EventEmitter {
39
+ constructor(agent, options) {
40
+ super();
41
+ this.session = null;
42
+ this.responseInFlight = false;
43
+ this.agent = agent;
44
+ this.options = options;
45
+ }
46
+ async connect(config) {
47
+ const audioElement = config.audioElement;
48
+ this.session = new RealtimeSession(this.agent, {
49
+ transport: new OpenAIRealtimeWebRTC({
50
+ audioElement,
51
+ ...this.options.codec === "g711" && {
52
+ changePeerConnection: async (pc) => {
53
+ pc.getTransceivers().forEach((transceiver) => {
54
+ if (transceiver.sender.track?.kind === "audio") {
55
+ transceiver.setCodecPreferences([
56
+ { mimeType: "audio/PCMU", clockRate: 8e3 },
57
+ { mimeType: "audio/PCMA", clockRate: 8e3 }
58
+ ]);
59
+ }
60
+ });
61
+ return pc;
62
+ }
63
+ }
64
+ }),
65
+ model: this.options.model || "gpt-realtime",
66
+ config: {
67
+ inputAudioFormat: this.options.codec === "g711" ? "g711_ulaw" : "pcm16",
68
+ outputAudioFormat: this.options.codec === "g711" ? "g711_ulaw" : "pcm16",
69
+ inputAudioTranscription: {
70
+ model: this.options.transcriptionModel || "gpt-4o-transcribe",
71
+ language: this.options.language || "en"
72
+ }
73
+ },
74
+ outputGuardrails: config.outputGuardrails ?? [],
75
+ context: config.context ?? {}
76
+ });
77
+ this.wireEvents(this.session);
78
+ await this.session.connect({ apiKey: config.authToken });
79
+ this.emit("status_change", "CONNECTED");
80
+ }
81
+ async disconnect() {
82
+ if (this.session) {
83
+ try {
84
+ await this.session.close();
85
+ } catch {
86
+ }
87
+ this.session = null;
88
+ }
89
+ this.removeAllListeners();
90
+ this.emit("status_change", "DISCONNECTED");
91
+ }
92
+ async sendMessage(text) {
93
+ if (!this.session) throw new Error("Session not connected");
94
+ if (this.responseInFlight) {
95
+ this.session.interrupt();
96
+ await new Promise((resolve) => {
97
+ const onDone = (event) => {
98
+ if (event.type === "response.done" || event.type === "response.cancelled") {
99
+ this.off("raw_event", onDone);
100
+ resolve();
101
+ }
102
+ };
103
+ this.on("raw_event", onDone);
104
+ setTimeout(resolve, 1500);
105
+ });
106
+ }
107
+ this.session.sendMessage(text);
108
+ }
109
+ interrupt() {
110
+ this.session?.interrupt();
111
+ }
112
+ mute(muted) {
113
+ this.session?.mute(muted);
114
+ }
115
+ sendRawEvent(event) {
116
+ this.session?.transport.sendEvent(event);
117
+ }
118
+ // Map OpenAI SDK events -> normalized SessionEvents
119
+ wireEvents(session) {
120
+ session.on("transport_event", (event) => {
121
+ const type = event.type;
122
+ switch (type) {
123
+ case "input_audio_buffer.speech_started":
124
+ this.emit("user_speech_started");
125
+ break;
126
+ case "conversation.item.input_audio_transcription.delta":
127
+ this.emit("user_transcript", {
128
+ itemId: event.item_id,
129
+ delta: event.delta || "",
130
+ isFinal: false
131
+ });
132
+ break;
133
+ case "conversation.item.input_audio_transcription.completed":
134
+ this.emit("user_transcript", {
135
+ itemId: event.item_id,
136
+ text: event.transcript || "",
137
+ isFinal: true
138
+ });
139
+ break;
140
+ case "response.audio_transcript.delta":
141
+ case "response.output_audio_transcript.delta":
142
+ this.emit("assistant_transcript", {
143
+ itemId: event.item_id,
144
+ delta: event.delta || "",
145
+ isFinal: false
146
+ });
147
+ break;
148
+ case "response.audio_transcript.done":
149
+ case "response.output_audio_transcript.done":
150
+ this.emit("assistant_transcript", {
151
+ itemId: event.item_id,
152
+ text: event.transcript || "",
153
+ isFinal: true
154
+ });
155
+ break;
156
+ case "response.audio.delta":
157
+ case "response.output_audio.delta":
158
+ this.emit("audio_delta", event.item_id, event.delta);
159
+ break;
160
+ case "response.created":
161
+ this.responseInFlight = true;
162
+ this.emit("raw_event", event);
163
+ break;
164
+ case "response.done":
165
+ this.responseInFlight = false;
166
+ this.emit("raw_event", event);
167
+ break;
168
+ case "conversation.item.truncated":
169
+ this.emit("raw_event", event);
170
+ break;
171
+ default:
172
+ this.emit("raw_event", event);
173
+ break;
174
+ }
175
+ });
176
+ session.on("agent_tool_start", ((...args) => {
177
+ const functionCall = args[2];
178
+ if (functionCall) {
179
+ this.emit("tool_call_start", functionCall.name, functionCall.arguments);
180
+ }
181
+ }));
182
+ session.on("agent_tool_end", ((...args) => {
183
+ const functionCall = args[2];
184
+ const result = args[3];
185
+ if (functionCall) {
186
+ this.emit("tool_call_end", functionCall.name, functionCall.arguments, result);
187
+ }
188
+ }));
189
+ session.on("agent_handoff", ((...args) => {
190
+ const item = args[0];
191
+ const context = item?.context;
192
+ const history = context?.history;
193
+ if (history?.length) {
194
+ const lastMessage = history[history.length - 1];
195
+ const agentName = (lastMessage.name || "").split("transfer_to_").pop() || "";
196
+ this.emit("agent_handoff", "", agentName);
197
+ }
198
+ }));
199
+ session.on("guardrail_tripped", ((...args) => {
200
+ this.emit("guardrail_tripped", args);
201
+ }));
202
+ session.on("history_updated", ((...args) => {
203
+ this.emit("raw_event", { type: "history_updated", items: args[0] });
204
+ }));
205
+ session.on("history_added", ((...args) => {
206
+ this.emit("raw_event", { type: "history_added", item: args[0] });
207
+ }));
208
+ session.on("error", (error) => {
209
+ if (error instanceof Error) {
210
+ this.emit("error", error);
211
+ } else if (error && typeof error === "object") {
212
+ const obj = error;
213
+ const msg = obj.message || obj.error?.message || JSON.stringify(error);
214
+ this.emit("error", new Error(msg));
215
+ } else {
216
+ this.emit("error", new Error(String(error)));
217
+ }
218
+ });
219
+ }
220
+ };
221
+ function openai(options = {}) {
222
+ return {
223
+ name: "openai",
224
+ createSession(agentConfig, sessionOpts) {
225
+ const merged = { ...options, ...sessionOpts };
226
+ const agent = buildRealtimeAgent(agentConfig);
227
+ return new OpenAISession(agent, merged);
228
+ }
229
+ };
230
+ }
231
+ function buildRealtimeAgent(config) {
232
+ const tools = (config.tools || []).map(convertTool);
233
+ return new RealtimeAgent({
234
+ name: config.name,
235
+ instructions: config.instructions,
236
+ tools
237
+ });
238
+ }
239
+ function openaiServer(config = {}) {
240
+ const getSessionToken = async (overrides = {}) => {
241
+ const merged = { ...config, ...overrides };
242
+ const apiKey = merged.apiKey || process.env.OPENAI_API_KEY;
243
+ if (!apiKey) return { error: "OpenAI API key not configured" };
244
+ try {
245
+ const response = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
246
+ method: "POST",
247
+ headers: {
248
+ Authorization: `Bearer ${apiKey}`,
249
+ "Content-Type": "application/json"
250
+ },
251
+ body: JSON.stringify({
252
+ expires_after: {
253
+ anchor: "created_at",
254
+ seconds: merged.expiresIn || 600
255
+ },
256
+ session: {
257
+ type: "realtime",
258
+ model: merged.model || "gpt-realtime",
259
+ ...merged.voice && { audio: { output: { voice: merged.voice } } },
260
+ ...merged.instructions && { instructions: merged.instructions }
261
+ }
262
+ })
263
+ });
264
+ if (!response.ok) {
265
+ const text = await response.text();
266
+ console.error("OpenAI client_secrets error:", text);
267
+ return { error: `OpenAI API error: ${response.status}` };
268
+ }
269
+ const data = await response.json();
270
+ if (!data.value) return { error: "Invalid response from OpenAI" };
271
+ return { token: data.value };
272
+ } catch (err) {
273
+ return { error: String(err) };
274
+ }
275
+ };
276
+ return {
277
+ getSessionToken,
278
+ createSessionHandler(overrides) {
279
+ return async (_request) => {
280
+ const result = await getSessionToken(overrides);
281
+ if (result.error) {
282
+ return Response.json({ error: result.error }, { status: 500 });
283
+ }
284
+ return Response.json({ ephemeralKey: result.token });
285
+ };
286
+ }
287
+ };
288
+ }
289
+ var openai_default = openai;
290
+ export {
291
+ openai_default as default,
292
+ openai,
293
+ openaiServer
294
+ };
@@ -0,0 +1,33 @@
1
+ // src/core/EventEmitter.ts
2
+ var EventEmitter = class {
3
+ constructor() {
4
+ this.handlers = /* @__PURE__ */ new Map();
5
+ }
6
+ on(event, handler) {
7
+ let set = this.handlers.get(event);
8
+ if (!set) {
9
+ set = /* @__PURE__ */ new Set();
10
+ this.handlers.set(event, set);
11
+ }
12
+ set.add(handler);
13
+ }
14
+ off(event, handler) {
15
+ this.handlers.get(event)?.delete(handler);
16
+ }
17
+ emit(event, ...args) {
18
+ this.handlers.get(event)?.forEach((fn) => {
19
+ try {
20
+ fn(...args);
21
+ } catch (e) {
22
+ console.error(`EventEmitter error in "${event}":`, e);
23
+ }
24
+ });
25
+ }
26
+ removeAllListeners() {
27
+ this.handlers.clear();
28
+ }
29
+ };
30
+
31
+ export {
32
+ EventEmitter
33
+ };
@@ -0,0 +1,178 @@
1
+ // src/tools.ts
2
+ function defineTool(config) {
3
+ return {
4
+ name: config.name,
5
+ description: config.description,
6
+ parameters: {
7
+ type: "object",
8
+ properties: config.parameters,
9
+ required: config.required
10
+ },
11
+ execute: config.execute
12
+ };
13
+ }
14
+ function createNavigationTool(sections) {
15
+ return defineTool({
16
+ name: "navigate",
17
+ description: `Navigate to a section. Available: ${sections.join(", ")}`,
18
+ parameters: {
19
+ section: {
20
+ type: "string",
21
+ enum: sections,
22
+ description: "Section to scroll to"
23
+ }
24
+ },
25
+ required: ["section"],
26
+ execute: ({ section }) => {
27
+ if (typeof window !== "undefined") {
28
+ const el = document.getElementById(section);
29
+ if (el) {
30
+ el.scrollIntoView({ behavior: "smooth" });
31
+ return { success: true, section };
32
+ }
33
+ }
34
+ return { success: false, error: "Section not found" };
35
+ }
36
+ });
37
+ }
38
+ function createEventTool(config) {
39
+ return defineTool({
40
+ name: config.name,
41
+ description: config.description,
42
+ parameters: config.parameters,
43
+ required: config.required,
44
+ execute: (params) => {
45
+ if (typeof window !== "undefined") {
46
+ window.dispatchEvent(new CustomEvent(config.eventType, {
47
+ detail: { toolName: config.name, params }
48
+ }));
49
+ }
50
+ return { success: true, ...params };
51
+ }
52
+ });
53
+ }
54
+ function createAPITool(config) {
55
+ return defineTool({
56
+ name: config.name,
57
+ description: config.description,
58
+ parameters: config.parameters,
59
+ required: config.required,
60
+ execute: async (params) => {
61
+ try {
62
+ const url = typeof config.endpoint === "function" ? config.endpoint(params) : config.endpoint;
63
+ const isPost = config.method === "POST";
64
+ const response = await fetch(url, {
65
+ method: config.method || "GET",
66
+ headers: {
67
+ ...isPost ? { "Content-Type": "application/json" } : {},
68
+ ...config.headers
69
+ },
70
+ body: isPost ? JSON.stringify(params) : void 0
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`HTTP ${response.status}`);
74
+ }
75
+ const data = await response.json();
76
+ return config.transform ? config.transform(data) : data;
77
+ } catch (error) {
78
+ return { success: false, error: String(error) };
79
+ }
80
+ }
81
+ });
82
+ }
83
+ function createSearchTool(config) {
84
+ const paramName = config.searchParam || "query";
85
+ return defineTool({
86
+ name: config.name,
87
+ description: config.description,
88
+ parameters: {
89
+ [paramName]: {
90
+ type: "string",
91
+ description: `The ${paramName} to search for`
92
+ }
93
+ },
94
+ required: [paramName],
95
+ execute: async (params) => {
96
+ const query = params[paramName];
97
+ try {
98
+ let result;
99
+ if (config.fetch) {
100
+ result = await config.fetch(query);
101
+ } else if (config.endpoint) {
102
+ const res = await fetch(config.endpoint, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ query })
106
+ });
107
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
108
+ result = await res.json();
109
+ } else {
110
+ throw new Error("Must provide either endpoint or fetch function");
111
+ }
112
+ const finalResult = config.transform ? config.transform(result) : result;
113
+ if (config.eventType && typeof window !== "undefined") {
114
+ window.dispatchEvent(new CustomEvent(config.eventType, {
115
+ detail: { query, result: finalResult }
116
+ }));
117
+ }
118
+ return finalResult;
119
+ } catch (error) {
120
+ return { success: false, error: String(error) };
121
+ }
122
+ }
123
+ });
124
+ }
125
+ function createRAGTool(config) {
126
+ return defineTool({
127
+ name: config.name,
128
+ description: config.description,
129
+ parameters: {
130
+ query: { type: "string", description: "Search query" },
131
+ ...config.repo ? {} : { repo: { type: "string", description: "Optional: filter by repository name" } }
132
+ },
133
+ required: ["query"],
134
+ execute: async (params) => {
135
+ const { query, repo } = params;
136
+ try {
137
+ const res = await fetch(config.endpoint, {
138
+ method: "POST",
139
+ headers: { "Content-Type": "application/json" },
140
+ body: JSON.stringify({
141
+ query,
142
+ repo: config.repo || repo,
143
+ limit: config.limit || 10
144
+ })
145
+ });
146
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
147
+ const result = await res.json();
148
+ if (config.eventType && typeof window !== "undefined") {
149
+ window.dispatchEvent(new CustomEvent(config.eventType, {
150
+ detail: { query, result }
151
+ }));
152
+ }
153
+ return result;
154
+ } catch (error) {
155
+ return { success: false, error: String(error) };
156
+ }
157
+ }
158
+ });
159
+ }
160
+ var TOOL_RESULT_EVENT = "voicekit:tool-result";
161
+ function emitToolResult(name, input, result) {
162
+ if (typeof window !== "undefined") {
163
+ window.dispatchEvent(new CustomEvent(TOOL_RESULT_EVENT, {
164
+ detail: { name, input, result, timestamp: Date.now() }
165
+ }));
166
+ }
167
+ }
168
+
169
+ export {
170
+ defineTool,
171
+ createNavigationTool,
172
+ createEventTool,
173
+ createAPITool,
174
+ createSearchTool,
175
+ createRAGTool,
176
+ TOOL_RESULT_EVENT,
177
+ emitToolResult
178
+ };
@@ -0,0 +1,33 @@
1
+ // src/core/EventEmitter.ts
2
+ var EventEmitter = class {
3
+ constructor() {
4
+ this.handlers = /* @__PURE__ */ new Map();
5
+ }
6
+ on(event, handler) {
7
+ let set = this.handlers.get(event);
8
+ if (!set) {
9
+ set = /* @__PURE__ */ new Set();
10
+ this.handlers.set(event, set);
11
+ }
12
+ set.add(handler);
13
+ }
14
+ off(event, handler) {
15
+ this.handlers.get(event)?.delete(handler);
16
+ }
17
+ emit(event, ...args) {
18
+ this.handlers.get(event)?.forEach((fn) => {
19
+ try {
20
+ fn(...args);
21
+ } catch (e) {
22
+ console.error(`EventEmitter error in "${String(event)}":`, e);
23
+ }
24
+ });
25
+ }
26
+ removeAllListeners() {
27
+ this.handlers.clear();
28
+ }
29
+ };
30
+
31
+ export {
32
+ EventEmitter
33
+ };
@@ -0,0 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ export {
9
+ __require
10
+ };