| 1 | require(Modules.Grok); |
| 2 | require(Modules.ApplicationStorage); |
| 3 | |
| 4 | const SYSTEM_PROMPT = ` |
| 5 | Your name is Voxi. You are a helpful voice assistant for phone callers representing the company Voximplant (pronounced VOX-im-plant). |
| 6 | Keep responses short and telephony-friendly (usually 1-2 sentences). |
| 7 | If the user asks for a live agent or an operator, call the "forward_to_agent" function. |
| 8 | If the user says goodbye, call the "hangup_call" function. |
| 9 | `; |
| 10 | |
| 11 | // -------------------- Grok Voice Agent settings -------------------- |
| 12 | const SESSION_PARAMETERS = { |
| 13 | session: { |
| 14 | voice: "Ara", |
| 15 | turn_detection: {type: "server_vad"}, |
| 16 | instructions: SYSTEM_PROMPT, |
| 17 | tools: [ |
| 18 | {type: "web_search"}, |
| 19 | { |
| 20 | type: "file_search", |
| 21 | vector_store_ids: ["collection_4c5a63ab-f739-4c13-93d2-05b74095c34a"], // optional RAG |
| 22 | max_num_results: 5, |
| 23 | }, |
| 24 | { |
| 25 | type: "x_search", |
| 26 | allowed_x_handles: ["voximplant", "aylarov"], |
| 27 | }, |
| 28 | { |
| 29 | type: "function", |
| 30 | name: "forward_to_agent", |
| 31 | description: "Forward the user to a live agent", |
| 32 | parameters: { |
| 33 | type: "object", |
| 34 | properties: {}, |
| 35 | required: [], |
| 36 | }, |
| 37 | }, |
| 38 | { |
| 39 | type: "function", |
| 40 | name: "hangup_call", |
| 41 | description: "Hangup the call", |
| 42 | parameters: { |
| 43 | type: "object", |
| 44 | properties: {}, |
| 45 | required: [], |
| 46 | }, |
| 47 | }, |
| 48 | ], |
| 49 | }, |
| 50 | }; |
| 51 | |
| 52 | // Helper to parse custom data JSON from call setup |
| 53 | function parseCustomData() { |
| 54 | const raw = VoxEngine.customData(); |
| 55 | if (!raw) return {}; |
| 56 | try { |
| 57 | return JSON.parse(raw); |
| 58 | } catch (error) { |
| 59 | Logger.write("===CUSTOM_DATA_PARSE_FAILED==="); |
| 60 | Logger.write(error); |
| 61 | return {}; |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | VoxEngine.addEventListener(AppEvents.Started, async () => { |
| 66 | let voiceAIClient; |
| 67 | let call; |
| 68 | let hangupCall = false; |
| 69 | let forwardToLiveAgent = false; |
| 70 | |
| 71 | const {destination, callerId} = parseCustomData(); |
| 72 | |
| 73 | if (!destination || !callerId) { |
| 74 | Logger.write( |
| 75 | "Missing destination or callerId. Provide custom data: {\"destination\":\"+16174018889\",\"callerId\":\"+13322776633\"}", |
| 76 | ); |
| 77 | VoxEngine.terminate(); |
| 78 | return; |
| 79 | } |
| 80 | |
| 81 | call = VoxEngine.callPSTN(destination, callerId); |
| 82 | // Alternative outbound paths (uncomment to use): |
| 83 | // call = VoxEngine.callUser({username: destination, callerid: callerId}); |
| 84 | // call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId); |
| 85 | // call = VoxEngine.callWhatsappUser({number: destination, callerid: callerId}); |
| 86 | |
| 87 | // Termination functions - add cleanup and logging as needed |
| 88 | call.addEventListener(CallEvents.Disconnected, ()=>VoxEngine.terminate()); |
| 89 | call.addEventListener(CallEvents.Failed, ()=>VoxEngine.terminate()); |
| 90 | |
| 91 | try { |
| 92 | voiceAIClient = await Grok.createVoiceAgentAPIClient({ |
| 93 | xAIApiKey: (await ApplicationStorage.get("XAI_API_KEY")).value, |
| 94 | onWebSocketClose: (event) => { |
| 95 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 96 | VoxEngine.terminate(); |
| 97 | }, |
| 98 | }); |
| 99 | |
| 100 | voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ConversationCreated, (event) => { |
| 101 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 102 | voiceAIClient.sessionUpdate(SESSION_PARAMETERS); |
| 103 | }); |
| 104 | |
| 105 | voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.SessionUpdated, (event) => { |
| 106 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 107 | voiceAIClient.responseCreate({instructions: "Hello."}); |
| 108 | }); |
| 109 | |
| 110 | // Keep it interruption-friendly (barge-in). |
| 111 | voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStarted, (event) => { |
| 112 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 113 | voiceAIClient.clearMediaBuffer(); |
| 114 | }); |
| 115 | |
| 116 | // Function calling. |
| 117 | voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ResponseFunctionCallArgumentsDone, (event) => { |
| 118 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 119 | |
| 120 | const {name, call_id} = event?.data?.payload || {}; |
| 121 | let output; |
| 122 | |
| 123 | if (name !== "forward_to_agent" && name !== "hangup_call") { |
| 124 | Logger.write(`===Ignoring unhandled function call: ${name}===`); |
| 125 | return; |
| 126 | } |
| 127 | |
| 128 | if (name === "forward_to_agent") { |
| 129 | forwardToLiveAgent = true; |
| 130 | output = {result: "Forwarding your call to a live agent. Please hold on."}; |
| 131 | } else if (name === "hangup_call") { |
| 132 | hangupCall = true; |
| 133 | output = {result: "Have a great day, goodbye!"}; |
| 134 | } |
| 135 | |
| 136 | voiceAIClient.conversationItemCreate({ |
| 137 | item: { |
| 138 | type: "function_call_output", |
| 139 | call_id, |
| 140 | output: JSON.stringify(output), |
| 141 | }, |
| 142 | }); |
| 143 | voiceAIClient.responseCreate({}); |
| 144 | }); |
| 145 | |
| 146 | // -------------------- Log Other Events -------------------- |
| 147 | [ |
| 148 | CallEvents.FirstAudioPacketReceived, |
| 149 | Grok.Events.WebSocketMediaStarted, |
| 150 | Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStopped, |
| 151 | Grok.VoiceAgentAPIEvents.ConversationItemInputAudioTranscriptionCompleted, |
| 152 | Grok.VoiceAgentAPIEvents.ConversationItemAdded, |
| 153 | Grok.VoiceAgentAPIEvents.ResponseCreated, |
| 154 | Grok.VoiceAgentAPIEvents.ResponseOutputItemAdded, |
| 155 | Grok.VoiceAgentAPIEvents.ResponseDone, |
| 156 | Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDelta, |
| 157 | Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDone, |
| 158 | Grok.VoiceAgentAPIEvents.ResponseOutputAudioDelta, |
| 159 | Grok.VoiceAgentAPIEvents.ResponseOutputAudioDone, |
| 160 | Grok.VoiceAgentAPIEvents.ResponseOutputItemDone, |
| 161 | Grok.VoiceAgentAPIEvents.ConnectorInformation, |
| 162 | Grok.VoiceAgentAPIEvents.InputAudioBufferCommitted, |
| 163 | Grok.VoiceAgentAPIEvents.WebSocketError, |
| 164 | Grok.VoiceAgentAPIEvents.Unknown, |
| 165 | ].forEach((evtName) => { |
| 166 | voiceAIClient.addEventListener(evtName, (e) => { |
| 167 | Logger.write(`===${e.name}===>${JSON.stringify(e)}`); |
| 168 | }); |
| 169 | }); |
| 170 | |
| 171 | voiceAIClient.addEventListener(Grok.Events.WebSocketMediaStarted, () => { |
| 172 | VoxEngine.sendMediaBetween(call, voiceAIClient); |
| 173 | }); |
| 174 | |
| 175 | voiceAIClient.addEventListener(Grok.Events.WebSocketMediaEnded, (event) => { |
| 176 | Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`); |
| 177 | if (hangupCall) callCloseHandler(); |
| 178 | else if (forwardToLiveAgent) { |
| 179 | call.say("Here is where I would forward the call via the phone network or SIP."); |
| 180 | call.addEventListener(CallEvents.PlaybackFinished, callCloseHandler); |
| 181 | } else return; |
| 182 | }); |
| 183 | } catch (error) { |
| 184 | Logger.write("===SOMETHING_WENT_WRONG==="); |
| 185 | Logger.write(error); |
| 186 | VoxEngine.terminate(); |
| 187 | } |
| 188 | }); |