@glydeunity/voice-sdk 1.2.2 → 1.2.3

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.
@@ -174,16 +174,16 @@ class g {
174
174
  * @returns Voice configuration including system prompt, tools, and Deepgram settings
175
175
  */
176
176
  async fetchConfig() {
177
- const e = `${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`, t = this.config.contextId ? `${e}/${this.config.contextId}` : e, s = await fetch(t, {
177
+ const e = `${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`, t = this.config.contextId ? `${e}/${this.config.contextId}` : e, i = await fetch(t, {
178
178
  method: "GET",
179
179
  headers: this.getAuthHeaders()
180
180
  });
181
- if (!s.ok) {
182
- const a = await s.json();
183
- throw new Error(a.error?.message || a.message || "Failed to fetch voice config");
181
+ if (!i.ok) {
182
+ const o = await i.json();
183
+ throw new Error(o.error?.message || o.message || "Failed to fetch voice config");
184
184
  }
185
- const { data: o } = await s.json();
186
- return o;
185
+ const { data: a } = await i.json();
186
+ return a;
187
187
  }
188
188
  /**
189
189
  * Initialize and start the voice session
@@ -204,14 +204,14 @@ class g {
204
204
  body: JSON.stringify(e)
205
205
  });
206
206
  if (!t.ok) {
207
- const i = await t.json();
208
- throw new Error(i.error?.message || i.message || "Failed to authenticate voice session");
207
+ const s = await t.json();
208
+ throw new Error(s.error?.message || s.message || "Failed to authenticate voice session");
209
209
  }
210
- const { data: s } = await t.json(), { token: o, agent_config: a, deepgram_config: r } = s, l = this.config.systemPrompt || a.instructions || this.serverConfig?.system_prompt || "You are a helpful AI assistant.";
210
+ const { data: i } = await t.json(), { token: a, agent_config: o, deepgram_config: r } = i, h = this.config.systemPrompt || o.instructions || this.serverConfig?.system_prompt || "You are a helpful AI assistant.";
211
211
  await this.initializeAudio();
212
- const c = "wss://agent.deepgram.com/v1/agent/converse";
213
- this.ws = new WebSocket(c, ["bearer", o]), this.ws.onopen = () => {
214
- const i = this.config.deepgramConfig || r || this.serverConfig?.deepgram_config || {
212
+ const p = "wss://agent.deepgram.com/v1/agent/converse";
213
+ this.ws = new WebSocket(p, ["bearer", a]), this.ws.onopen = () => {
214
+ const s = this.config.deepgramConfig || r || this.serverConfig?.deepgram_config || {
215
215
  think: {
216
216
  provider: { type: "open_ai", model: "gpt-4.1-mini" },
217
217
  functions: [
@@ -241,12 +241,43 @@ End the conversation immediately if:
241
241
  },
242
242
  required: ["item"]
243
243
  }
244
+ },
245
+ {
246
+ name: "other_opportunities",
247
+ description: `You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
248
+
249
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
250
+
251
+ Here is a list of phrases to listen for but not restricted to:
252
+ -other opportunities
253
+ -other jobs
254
+ -other roles
255
+ -other job opportunities
256
+ -other job roles
257
+ -other job opportunities
258
+
259
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
260
+
261
+ Suggest other opportunities if:
262
+ 1. The user's input exactly matches any phrase in the list.
263
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
264
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
265
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
266
+
267
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,
268
+ parameters: {
269
+ type: "object",
270
+ properties: {
271
+ item: { type: "string", description: "The phrase or text that triggered the suggestion of other opportunities" }
272
+ },
273
+ required: ["item"]
274
+ }
244
275
  }
245
276
  ]
246
277
  },
247
278
  speak: { provider: { type: "deepgram", model: "aura-2-thalia-en" } },
248
279
  listen: { provider: { type: "deepgram", version: "v2", model: "flux-general-en" } }
249
- }, h = {
280
+ }, l = {
250
281
  type: "Settings",
251
282
  audio: {
252
283
  input: {
@@ -261,15 +292,15 @@ End the conversation immediately if:
261
292
  },
262
293
  agent: {
263
294
  language: "en",
264
- speak: i.speak || {
295
+ speak: s.speak || {
265
296
  provider: { type: "deepgram", model: "aura-2-thalia-en" }
266
297
  },
267
- listen: i.listen || {
298
+ listen: s.listen || {
268
299
  provider: { type: "deepgram", version: "v2", model: "flux-general-en" }
269
300
  },
270
301
  think: {
271
- provider: i.think?.provider || { type: "open_ai", model: "gpt-4.1-mini" },
272
- functions: i.think?.functions || [
302
+ provider: s.think?.provider || { type: "open_ai", model: "gpt-4.1-mini" },
303
+ functions: s.think?.functions || [
273
304
  {
274
305
  name: "end_conversation",
275
306
  description: `You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
@@ -296,31 +327,62 @@ End the conversation immediately if:
296
327
  },
297
328
  required: ["item"]
298
329
  }
330
+ },
331
+ {
332
+ name: "other_opportunities",
333
+ description: `You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
334
+
335
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
336
+
337
+ Here is a list of phrases to listen for but not restricted to:
338
+ -other opportunities
339
+ -other jobs
340
+ -other roles
341
+ -other job opportunities
342
+ -other job roles
343
+ -other job opportunities
344
+
345
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
346
+
347
+ Suggest other opportunities if:
348
+ 1. The user's input exactly matches any phrase in the list.
349
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
350
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
351
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
352
+
353
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,
354
+ parameters: {
355
+ type: "object",
356
+ properties: {
357
+ item: { type: "string", description: "The phrase or text that triggered the suggestion of other opportunities" }
358
+ },
359
+ required: ["item"]
360
+ }
299
361
  }
300
362
  ]
301
363
  },
302
- greeting: "Hi! I'm ready to speak with you. How can I help you today?"
364
+ greeting: "Hi! I'm excited you chose to speak with me. Are you ready to start?"
303
365
  }
304
366
  };
305
- this.ws.send(JSON.stringify(h)), this.emit({ type: "open", payload: { config: a, serverConfig: this.serverConfig } });
367
+ this.ws.send(JSON.stringify(l)), this.emit({ type: "open", payload: { config: o, serverConfig: this.serverConfig } });
306
368
  };
307
- const n = l;
308
- this.ws.onmessage = (i) => {
309
- if (typeof i.data == "string") {
369
+ const n = h;
370
+ this.ws.onmessage = (s) => {
371
+ if (typeof s.data == "string") {
310
372
  try {
311
- if (JSON.parse(i.data).type === "SettingsApplied") {
312
- const p = {
373
+ if (JSON.parse(s.data).type === "SettingsApplied") {
374
+ const c = {
313
375
  type: "UpdatePrompt",
314
376
  prompt: n
315
377
  };
316
- this.ws.send(JSON.stringify(p)), this.startMicrophone();
378
+ this.ws.send(JSON.stringify(c)), this.startMicrophone();
317
379
  }
318
380
  } catch {
319
381
  }
320
- this.handleTextMessage(i.data);
321
- } else i.data instanceof Blob ? this.handleAudioData(i.data) : i.data instanceof ArrayBuffer && this.handleAudioBuffer(i.data);
322
- }, this.ws.onerror = (i) => {
323
- console.error("[GlydeVoice] WebSocket error:", i), this.emit({ type: "error", payload: i });
382
+ this.handleTextMessage(s.data);
383
+ } else s.data instanceof Blob ? this.handleAudioData(s.data) : s.data instanceof ArrayBuffer && this.handleAudioBuffer(s.data);
384
+ }, this.ws.onerror = (s) => {
385
+ console.error("[GlydeVoice] WebSocket error:", s), this.emit({ type: "error", payload: s });
324
386
  }, this.ws.onclose = () => {
325
387
  this.cleanup(), this.emit({ type: "close" });
326
388
  }, this.renderUI();
@@ -354,9 +416,9 @@ End the conversation immediately if:
354
416
  } finally {
355
417
  URL.revokeObjectURL(e), URL.revokeObjectURL(t);
356
418
  }
357
- this.playbackWorkletNode = new AudioWorkletNode(this.audioContext, "audio-playback-processor"), this.playbackWorkletNode.connect(this.audioContext.destination), this.playbackWorkletNode.port.onmessage = (s) => {
358
- const { type: o } = s.data;
359
- (o === "cleared" || o === "bufferEmpty") && (this.isAgentSpeaking = !1, this.agentAudioDoneReceived = !1, this.emit({ type: "agent_speaking", payload: !1 }));
419
+ this.playbackWorkletNode = new AudioWorkletNode(this.audioContext, "audio-playback-processor"), this.playbackWorkletNode.connect(this.audioContext.destination), this.playbackWorkletNode.port.onmessage = (i) => {
420
+ const { type: a } = i.data;
421
+ (a === "cleared" || a === "bufferEmpty") && (this.isAgentSpeaking = !1, this.agentAudioDoneReceived = !1, this.emit({ type: "agent_speaking", payload: !1 }));
360
422
  };
361
423
  }
362
424
  /**
@@ -379,8 +441,8 @@ End the conversation immediately if:
379
441
  break;
380
442
  case "ConversationText":
381
443
  if (t.content && t.content.trim()) {
382
- const s = t.role === "assistant" ? "agent" : "user";
383
- this.config.onTranscript && this.config.onTranscript(t.content, s), this.emit({ type: "transcript", payload: { text: t.content, role: s } }), this.saveTranscript(t.content, t.role);
444
+ const i = t.role === "assistant" ? "agent" : "user";
445
+ this.config.onTranscript && this.config.onTranscript(t.content, i), this.emit({ type: "transcript", payload: { text: t.content, role: i } }), this.saveTranscript(t.content, t.role);
384
446
  }
385
447
  break;
386
448
  case "AgentStartedSpeaking":
@@ -413,30 +475,30 @@ End the conversation immediately if:
413
475
  this.audioContext.state === "suspended" && this.audioContext.resume();
414
476
  const t = e.byteLength;
415
477
  if (t === 0) return;
416
- const s = t - t % 2;
417
- if (s === 0) return;
418
- const o = s === t ? e : e.slice(0, s), a = new Int16Array(o), r = new Float32Array(a.length);
419
- for (let n = 0; n < a.length; n++)
420
- r[n] = a[n] / 32768;
421
- const l = this.resample24kTo48k(r);
478
+ const i = t - t % 2;
479
+ if (i === 0) return;
480
+ const a = i === t ? e : e.slice(0, i), o = new Int16Array(a), r = new Float32Array(o.length);
481
+ for (let n = 0; n < o.length; n++)
482
+ r[n] = o[n] / 32768;
483
+ const h = this.resample24kTo48k(r);
422
484
  !this.isAgentSpeaking && !this.agentAudioDoneReceived && (this.isAgentSpeaking = !0, this.emit({ type: "agent_speaking", payload: !0 }));
423
- const c = new Float32Array(l);
485
+ const p = new Float32Array(h);
424
486
  this.playbackWorkletNode.port.postMessage({
425
487
  type: "audio",
426
- data: c
427
- }, [c.buffer]);
488
+ data: p
489
+ }, [p.buffer]);
428
490
  }
429
491
  /**
430
492
  * Resample audio from 24kHz to 48kHz using linear interpolation
431
493
  */
432
494
  resample24kTo48k(e) {
433
- const t = e.length * 2, s = new Float32Array(t);
434
- for (let a = 0; a < e.length - 1; a++) {
435
- const r = e[a], l = e[a + 1];
436
- s[a * 2] = r, s[a * 2 + 1] = (r + l) / 2;
495
+ const t = e.length * 2, i = new Float32Array(t);
496
+ for (let o = 0; o < e.length - 1; o++) {
497
+ const r = e[o], h = e[o + 1];
498
+ i[o * 2] = r, i[o * 2 + 1] = (r + h) / 2;
437
499
  }
438
- const o = e.length - 1;
439
- return s[o * 2] = e[o], s[o * 2 + 1] = e[o], s;
500
+ const a = e.length - 1;
501
+ return i[a * 2] = e[a], i[a * 2 + 1] = e[a], i;
440
502
  }
441
503
  /**
442
504
  * Clear the playback buffer (for interruption handling)
@@ -1,4 +1,4 @@
1
- (function(n,c){typeof exports=="object"&&typeof module<"u"?c(exports):typeof define=="function"&&define.amd?define(["exports"],c):(n=typeof globalThis<"u"?globalThis:n||self,c(n.GlydeVoice={}))})(this,(function(n){"use strict";const c=`
1
+ (function(n,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(n=typeof globalThis<"u"?globalThis:n||self,l(n.GlydeVoice={}))})(this,(function(n){"use strict";const l=`
2
2
  class AudioCaptureProcessor extends AudioWorkletProcessor {
3
3
  constructor() {
4
4
  super();
@@ -130,7 +130,7 @@ class AudioPlaybackProcessor extends AudioWorkletProcessor {
130
130
  }
131
131
 
132
132
  registerProcessor('audio-playback-processor', AudioPlaybackProcessor);
133
- `;class f{config;unityUrl;active=!1;serverConfig=null;ws=null;audioContext=null;mediaStream=null;captureWorkletNode=null;playbackWorkletNode=null;isMuted=!1;outputSampleRate=24e3;inputSampleRate=48e3;isAgentSpeaking=!1;agentAudioDoneReceived=!1;constructor(e){this.config=e,this.unityUrl=e.unityBaseUrl||"https://api.glydeunity.com",!e.publishableKey&&!e.apiKey&&!e.authToken&&console.warn("[GlydeVoice] No authentication method provided. One of publishableKey, apiKey, or authToken is required.")}getAuthHeaders(){const e={"Content-Type":"application/json"};return this.config.publishableKey&&(e["x-publishable-key"]=this.config.publishableKey),this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.authToken&&(e.Authorization=`Bearer ${this.config.authToken}`),e}async fetchConfig(){const e=`${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`,t=this.config.contextId?`${e}/${this.config.contextId}`:e,i=await fetch(t,{method:"GET",headers:this.getAuthHeaders()});if(!i.ok){const o=await i.json();throw new Error(o.error?.message||o.message||"Failed to fetch voice config")}const{data:a}=await i.json();return a}async start(){if(!this.active){this.active=!0;try{this.config.systemPrompt||(this.serverConfig=await this.fetchConfig(),console.log("[GlydeVoice] Fetched config:",this.serverConfig));const e={context_id:this.config.contextId,domain:typeof window<"u"?window.location.hostname:"localhost"};this.config.systemPrompt&&(e.system_prompt=this.config.systemPrompt),this.config.deepgramConfig&&(e.deepgram_config=this.config.deepgramConfig);const t=await fetch(`${this.unityUrl}/api/unity/voice/auth`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify(e)});if(!t.ok){const s=await t.json();throw new Error(s.error?.message||s.message||"Failed to authenticate voice session")}const{data:i}=await t.json(),{token:a,agent_config:o,deepgram_config:r}=i,h=this.config.systemPrompt||o.instructions||this.serverConfig?.system_prompt||"You are a helpful AI assistant.";await this.initializeAudio();const p="wss://agent.deepgram.com/v1/agent/converse";this.ws=new WebSocket(p,["bearer",a]),this.ws.onopen=()=>{const s=this.config.deepgramConfig||r||this.serverConfig?.deepgram_config||{think:{provider:{type:"open_ai",model:"gpt-4.1-mini"},functions:[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
133
+ `;class f{config;unityUrl;active=!1;serverConfig=null;ws=null;audioContext=null;mediaStream=null;captureWorkletNode=null;playbackWorkletNode=null;isMuted=!1;outputSampleRate=24e3;inputSampleRate=48e3;isAgentSpeaking=!1;agentAudioDoneReceived=!1;constructor(e){this.config=e,this.unityUrl=e.unityBaseUrl||"https://api.glydeunity.com",!e.publishableKey&&!e.apiKey&&!e.authToken&&console.warn("[GlydeVoice] No authentication method provided. One of publishableKey, apiKey, or authToken is required.")}getAuthHeaders(){const e={"Content-Type":"application/json"};return this.config.publishableKey&&(e["x-publishable-key"]=this.config.publishableKey),this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.authToken&&(e.Authorization=`Bearer ${this.config.authToken}`),e}async fetchConfig(){const e=`${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`,t=this.config.contextId?`${e}/${this.config.contextId}`:e,i=await fetch(t,{method:"GET",headers:this.getAuthHeaders()});if(!i.ok){const o=await i.json();throw new Error(o.error?.message||o.message||"Failed to fetch voice config")}const{data:a}=await i.json();return a}async start(){if(!this.active){this.active=!0;try{this.config.systemPrompt||(this.serverConfig=await this.fetchConfig(),console.log("[GlydeVoice] Fetched config:",this.serverConfig));const e={context_id:this.config.contextId,domain:typeof window<"u"?window.location.hostname:"localhost"};this.config.systemPrompt&&(e.system_prompt=this.config.systemPrompt),this.config.deepgramConfig&&(e.deepgram_config=this.config.deepgramConfig);const t=await fetch(`${this.unityUrl}/api/unity/voice/auth`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify(e)});if(!t.ok){const s=await t.json();throw new Error(s.error?.message||s.message||"Failed to authenticate voice session")}const{data:i}=await t.json(),{token:a,agent_config:o,deepgram_config:r}=i,p=this.config.systemPrompt||o.instructions||this.serverConfig?.system_prompt||"You are a helpful AI assistant.";await this.initializeAudio();const c="wss://agent.deepgram.com/v1/agent/converse";this.ws=new WebSocket(c,["bearer",a]),this.ws.onopen=()=>{const s=this.config.deepgramConfig||r||this.serverConfig?.deepgram_config||{think:{provider:{type:"open_ai",model:"gpt-4.1-mini"},functions:[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
134
134
 
135
135
  Here is a list of phrases to listen for but not restricted to:
136
136
  -stop
@@ -146,7 +146,27 @@ When monitoring the conversation, pay close attention to any input that matches
146
146
  End the conversation immediately if:
147
147
  1. The user's input exactly matches any phrase in the list.
148
148
  2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
149
- 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}}]},speak:{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}}},d={type:"Settings",audio:{input:{encoding:"linear16",sample_rate:this.inputSampleRate},output:{encoding:"linear16",sample_rate:this.outputSampleRate,container:"none"}},agent:{language:"en",speak:s.speak||{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:s.listen||{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}},think:{provider:s.think?.provider||{type:"open_ai",model:"gpt-4.1-mini"},functions:s.think?.functions||[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
149
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}},{name:"other_opportunities",description:`You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
150
+
151
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
152
+
153
+ Here is a list of phrases to listen for but not restricted to:
154
+ -other opportunities
155
+ -other jobs
156
+ -other roles
157
+ -other job opportunities
158
+ -other job roles
159
+ -other job opportunities
160
+
161
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
162
+
163
+ Suggest other opportunities if:
164
+ 1. The user's input exactly matches any phrase in the list.
165
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
166
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
167
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
168
+
169
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the suggestion of other opportunities"}},required:["item"]}}]},speak:{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}}},d={type:"Settings",audio:{input:{encoding:"linear16",sample_rate:this.inputSampleRate},output:{encoding:"linear16",sample_rate:this.outputSampleRate,container:"none"}},agent:{language:"en",speak:s.speak||{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:s.listen||{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}},think:{provider:s.think?.provider||{type:"open_ai",model:"gpt-4.1-mini"},functions:s.think?.functions||[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
150
170
 
151
171
  Here is a list of phrases to listen for but not restricted to:
152
172
  -stop
@@ -162,7 +182,27 @@ When monitoring the conversation, pay close attention to any input that matches
162
182
  End the conversation immediately if:
163
183
  1. The user's input exactly matches any phrase in the list.
164
184
  2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
165
- 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}}]},greeting:"Hi! I'm ready to speak with you. How can I help you today?"}};this.ws.send(JSON.stringify(d)),this.emit({type:"open",payload:{config:o,serverConfig:this.serverConfig}})};const l=h;this.ws.onmessage=s=>{if(typeof s.data=="string"){try{if(JSON.parse(s.data).type==="SettingsApplied"){const g={type:"UpdatePrompt",prompt:l};this.ws.send(JSON.stringify(g)),this.startMicrophone()}}catch{}this.handleTextMessage(s.data)}else s.data instanceof Blob?this.handleAudioData(s.data):s.data instanceof ArrayBuffer&&this.handleAudioBuffer(s.data)},this.ws.onerror=s=>{console.error("[GlydeVoice] WebSocket error:",s),this.emit({type:"error",payload:s})},this.ws.onclose=()=>{this.cleanup(),this.emit({type:"close"})},this.renderUI()}catch(e){throw console.error("[GlydeVoice] Error starting session:",e),this.active=!1,this.emit({type:"error",payload:e}),e}}}createWorkletBlobUrl(e){const t=new Blob([e],{type:"application/javascript"});return URL.createObjectURL(t)}async initializeAudio(){this.audioContext=new AudioContext({sampleRate:this.inputSampleRate});const e=this.createWorkletBlobUrl(c),t=this.createWorkletBlobUrl(u);try{await Promise.all([this.audioContext.audioWorklet.addModule(e),this.audioContext.audioWorklet.addModule(t)])}finally{URL.revokeObjectURL(e),URL.revokeObjectURL(t)}this.playbackWorkletNode=new AudioWorkletNode(this.audioContext,"audio-playback-processor"),this.playbackWorkletNode.connect(this.audioContext.destination),this.playbackWorkletNode.port.onmessage=i=>{const{type:a}=i.data;(a==="cleared"||a==="bufferEmpty")&&(this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!1}))}}handleTextMessage(e){try{const t=JSON.parse(e);switch(t.type){case"Welcome":this.emit({type:"ready"});break;case"SettingsApplied":break;case"UserStartedSpeaking":this.emit({type:"user_speaking",payload:!0}),this.clearPlaybackBuffer(),this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1;break;case"UserStoppedSpeaking":this.emit({type:"user_speaking",payload:!1});break;case"ConversationText":if(t.content&&t.content.trim()){const i=t.role==="assistant"?"agent":"user";this.config.onTranscript&&this.config.onTranscript(t.content,i),this.emit({type:"transcript",payload:{text:t.content,role:i}}),this.saveTranscript(t.content,t.role)}break;case"AgentStartedSpeaking":this.isAgentSpeaking=!0,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!0});break;case"AgentAudioDone":this.agentAudioDoneReceived=!0;break;case"Error":console.error("[GlydeVoice] Agent error:",t),this.emit({type:"error",payload:t});break}}catch(t){console.error("[GlydeVoice] Failed to parse message:",t)}}async handleAudioData(e){const t=await e.arrayBuffer();this.handleAudioBuffer(t)}handleAudioBuffer(e){if(!this.playbackWorkletNode||!this.audioContext)return;this.audioContext.state==="suspended"&&this.audioContext.resume();const t=e.byteLength;if(t===0)return;const i=t-t%2;if(i===0)return;const a=i===t?e:e.slice(0,i),o=new Int16Array(a),r=new Float32Array(o.length);for(let l=0;l<o.length;l++)r[l]=o[l]/32768;const h=this.resample24kTo48k(r);!this.isAgentSpeaking&&!this.agentAudioDoneReceived&&(this.isAgentSpeaking=!0,this.emit({type:"agent_speaking",payload:!0}));const p=new Float32Array(h);this.playbackWorkletNode.port.postMessage({type:"audio",data:p},[p.buffer])}resample24kTo48k(e){const t=e.length*2,i=new Float32Array(t);for(let o=0;o<e.length-1;o++){const r=e[o],h=e[o+1];i[o*2]=r,i[o*2+1]=(r+h)/2}const a=e.length-1;return i[a*2]=e[a],i[a*2+1]=e[a],i}clearPlaybackBuffer(){this.playbackWorkletNode&&this.playbackWorkletNode.port.postMessage({type:"clear"})}async startMicrophone(){if(!this.audioContext)throw new Error("Audio context not initialized");try{this.mediaStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:this.inputSampleRate,echoCancellation:!0,noiseSuppression:!0}});const e=this.audioContext.createMediaStreamSource(this.mediaStream);this.captureWorkletNode=new AudioWorkletNode(this.audioContext,"audio-capture-processor"),this.captureWorkletNode.port.onmessage=t=>{!this.active||!this.ws||this.ws.readyState!==WebSocket.OPEN||this.isMuted||this.ws.send(t.data)},e.connect(this.captureWorkletNode),this.emit({type:"microphone_ready"})}catch(e){throw console.error("[GlydeVoice] Microphone error:",e),e}}async saveTranscript(e,t){if(!(!this.config.contextId||!e))try{await fetch(`${this.unityUrl}/api/unity/voice/transcript`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify({context_id:this.config.contextId,content:e,role:t==="assistant"?"assistant":"user"})})}catch{}}setMuted(e){this.isMuted=e}getMuted(){return this.isMuted}isActive(){return this.active}getServerConfig(){return this.serverConfig}stop(){this.active=!1,this.cleanup()}cleanup(){this.captureWorkletNode&&(this.captureWorkletNode.disconnect(),this.captureWorkletNode.port.close(),this.captureWorkletNode=null),this.playbackWorkletNode&&(this.playbackWorkletNode.disconnect(),this.playbackWorkletNode.port.close(),this.playbackWorkletNode=null),this.mediaStream&&(this.mediaStream.getTracks().forEach(e=>e.stop()),this.mediaStream=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.ws&&(this.ws.readyState===WebSocket.OPEN&&this.ws.close(),this.ws=null)}emit(e){this.config.onEvent&&this.config.onEvent(e)}renderUI(){if(!this.config.container)return;const e=typeof this.config.container=="string"?document.querySelector(this.config.container):this.config.container;e&&(e.innerHTML=`
185
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}},{name:"other_opportunities",description:`You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
186
+
187
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
188
+
189
+ Here is a list of phrases to listen for but not restricted to:
190
+ -other opportunities
191
+ -other jobs
192
+ -other roles
193
+ -other job opportunities
194
+ -other job roles
195
+ -other job opportunities
196
+
197
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
198
+
199
+ Suggest other opportunities if:
200
+ 1. The user's input exactly matches any phrase in the list.
201
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
202
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
203
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
204
+
205
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the suggestion of other opportunities"}},required:["item"]}}]},greeting:"Hi! I'm excited you chose to speak with me. Are you ready to start?"}};this.ws.send(JSON.stringify(d)),this.emit({type:"open",payload:{config:o,serverConfig:this.serverConfig}})};const h=p;this.ws.onmessage=s=>{if(typeof s.data=="string"){try{if(JSON.parse(s.data).type==="SettingsApplied"){const y={type:"UpdatePrompt",prompt:h};this.ws.send(JSON.stringify(y)),this.startMicrophone()}}catch{}this.handleTextMessage(s.data)}else s.data instanceof Blob?this.handleAudioData(s.data):s.data instanceof ArrayBuffer&&this.handleAudioBuffer(s.data)},this.ws.onerror=s=>{console.error("[GlydeVoice] WebSocket error:",s),this.emit({type:"error",payload:s})},this.ws.onclose=()=>{this.cleanup(),this.emit({type:"close"})},this.renderUI()}catch(e){throw console.error("[GlydeVoice] Error starting session:",e),this.active=!1,this.emit({type:"error",payload:e}),e}}}createWorkletBlobUrl(e){const t=new Blob([e],{type:"application/javascript"});return URL.createObjectURL(t)}async initializeAudio(){this.audioContext=new AudioContext({sampleRate:this.inputSampleRate});const e=this.createWorkletBlobUrl(l),t=this.createWorkletBlobUrl(u);try{await Promise.all([this.audioContext.audioWorklet.addModule(e),this.audioContext.audioWorklet.addModule(t)])}finally{URL.revokeObjectURL(e),URL.revokeObjectURL(t)}this.playbackWorkletNode=new AudioWorkletNode(this.audioContext,"audio-playback-processor"),this.playbackWorkletNode.connect(this.audioContext.destination),this.playbackWorkletNode.port.onmessage=i=>{const{type:a}=i.data;(a==="cleared"||a==="bufferEmpty")&&(this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!1}))}}handleTextMessage(e){try{const t=JSON.parse(e);switch(t.type){case"Welcome":this.emit({type:"ready"});break;case"SettingsApplied":break;case"UserStartedSpeaking":this.emit({type:"user_speaking",payload:!0}),this.clearPlaybackBuffer(),this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1;break;case"UserStoppedSpeaking":this.emit({type:"user_speaking",payload:!1});break;case"ConversationText":if(t.content&&t.content.trim()){const i=t.role==="assistant"?"agent":"user";this.config.onTranscript&&this.config.onTranscript(t.content,i),this.emit({type:"transcript",payload:{text:t.content,role:i}}),this.saveTranscript(t.content,t.role)}break;case"AgentStartedSpeaking":this.isAgentSpeaking=!0,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!0});break;case"AgentAudioDone":this.agentAudioDoneReceived=!0;break;case"Error":console.error("[GlydeVoice] Agent error:",t),this.emit({type:"error",payload:t});break}}catch(t){console.error("[GlydeVoice] Failed to parse message:",t)}}async handleAudioData(e){const t=await e.arrayBuffer();this.handleAudioBuffer(t)}handleAudioBuffer(e){if(!this.playbackWorkletNode||!this.audioContext)return;this.audioContext.state==="suspended"&&this.audioContext.resume();const t=e.byteLength;if(t===0)return;const i=t-t%2;if(i===0)return;const a=i===t?e:e.slice(0,i),o=new Int16Array(a),r=new Float32Array(o.length);for(let h=0;h<o.length;h++)r[h]=o[h]/32768;const p=this.resample24kTo48k(r);!this.isAgentSpeaking&&!this.agentAudioDoneReceived&&(this.isAgentSpeaking=!0,this.emit({type:"agent_speaking",payload:!0}));const c=new Float32Array(p);this.playbackWorkletNode.port.postMessage({type:"audio",data:c},[c.buffer])}resample24kTo48k(e){const t=e.length*2,i=new Float32Array(t);for(let o=0;o<e.length-1;o++){const r=e[o],p=e[o+1];i[o*2]=r,i[o*2+1]=(r+p)/2}const a=e.length-1;return i[a*2]=e[a],i[a*2+1]=e[a],i}clearPlaybackBuffer(){this.playbackWorkletNode&&this.playbackWorkletNode.port.postMessage({type:"clear"})}async startMicrophone(){if(!this.audioContext)throw new Error("Audio context not initialized");try{this.mediaStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:this.inputSampleRate,echoCancellation:!0,noiseSuppression:!0}});const e=this.audioContext.createMediaStreamSource(this.mediaStream);this.captureWorkletNode=new AudioWorkletNode(this.audioContext,"audio-capture-processor"),this.captureWorkletNode.port.onmessage=t=>{!this.active||!this.ws||this.ws.readyState!==WebSocket.OPEN||this.isMuted||this.ws.send(t.data)},e.connect(this.captureWorkletNode),this.emit({type:"microphone_ready"})}catch(e){throw console.error("[GlydeVoice] Microphone error:",e),e}}async saveTranscript(e,t){if(!(!this.config.contextId||!e))try{await fetch(`${this.unityUrl}/api/unity/voice/transcript`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify({context_id:this.config.contextId,content:e,role:t==="assistant"?"assistant":"user"})})}catch{}}setMuted(e){this.isMuted=e}getMuted(){return this.isMuted}isActive(){return this.active}getServerConfig(){return this.serverConfig}stop(){this.active=!1,this.cleanup()}cleanup(){this.captureWorkletNode&&(this.captureWorkletNode.disconnect(),this.captureWorkletNode.port.close(),this.captureWorkletNode=null),this.playbackWorkletNode&&(this.playbackWorkletNode.disconnect(),this.playbackWorkletNode.port.close(),this.playbackWorkletNode=null),this.mediaStream&&(this.mediaStream.getTracks().forEach(e=>e.stop()),this.mediaStream=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.ws&&(this.ws.readyState===WebSocket.OPEN&&this.ws.close(),this.ws=null)}emit(e){this.config.onEvent&&this.config.onEvent(e)}renderUI(){if(!this.config.container)return;const e=typeof this.config.container=="string"?document.querySelector(this.config.container):this.config.container;e&&(e.innerHTML=`
166
206
  <div style="padding: 20px; border: 1px solid #ccc; border-radius: 8px; background: #fff;">
167
207
  <h3>Glyde Voice Agent</h3>
168
208
  <p>Status: Active</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glydeunity/voice-sdk",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "GLYDE Voice Agent SDK - AI-powered voice interactions for web applications",
5
5
  "type": "module",
6
6
  "main": "./dist/voice-sdk.umd.js",