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