Skip to content

Commit

Permalink
Add call transfer example, make all functions async
Browse files Browse the repository at this point in the history
  • Loading branch information
cweems committed Mar 5, 2024
1 parent 1c6151d commit bf0f91a
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,6 @@ dist
.svelte-kit

# Ignore Fly.io configuration file
.toml
fly.toml

# End of https://www.toptal.com/developers/gitignore/api/node
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ For our `placeOrder` function, the arguments passed will look like this:
}
```
### Returning Arguments to GPT
Your function should always return a value: GPT tends to get confused when the function returns nothing, and may continue trying to call the function expecting an answer. If your function doesn't have any data to return to the GPT, you should still consider returning a response that says something like "The function to do (X) ran successfully."
Your function should always return a value: GPT will get confused when the function returns nothing, and may continue trying to call the function expecting an answer. If your function doesn't have any data to return to the GPT, you should still return a response with an instruction like "Tell the user that their request was processed successfully." This prevents the GPT from calling the function repeatedly and wasting tokens.

Any data that you return to the GPT should match the expected format listed in the `returns` key of `function-manifest.js`.

Expand Down
3 changes: 3 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ app.ws('/connection', (ws) => {
ws.on('error', console.error);
// Filled in from start message
let streamSid;
let callSid;

const gptService = new GptService();
const streamService = new StreamService(ws);
Expand All @@ -44,7 +45,9 @@ app.ws('/connection', (ws) => {
const msg = JSON.parse(data);
if (msg.event === 'start') {
streamSid = msg.start.streamSid;
callSid = msg.start.callSid;
streamService.setStreamSid(streamSid);
gptService.setCallSid(callSid);
console.log(`Twilio -> Starting Media Stream for ${streamSid}`.underline.red);
ttsService.generate({partialResponseIndex: null, partialResponse: 'Hello! I understand you\'re looking for a pair of AirPods, is that correct?'}, 1);
} else if (msg.event === 'media') {
Expand Down
2 changes: 1 addition & 1 deletion functions/checkInventory.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function checkInventory(functionArgs) {
async function checkInventory(functionArgs) {
const model = functionArgs.model;
console.log('GPT -> called checkInventory function');

Expand Down
2 changes: 1 addition & 1 deletion functions/checkPrice.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function checkPrice(functionArgs) {
async function checkPrice(functionArgs) {
let model = functionArgs.model;
console.log('GPT -> called checkPrice function');
if (model?.toLowerCase().includes('pro')) {
Expand Down
26 changes: 26 additions & 0 deletions functions/function-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,32 @@ const tools = [
}
},
},
{
type: 'function',
function: {
name: 'transferCall',
description: 'Transfers the customer to a live agent in case they request help from a real person.',
parameters: {
type: 'object',
properties: {
callSid: {
type: 'string',
description: 'The unique identifier for the active phone call.',
},
},
required: ['callSid'],
},
returns: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Whether or not the customer call was successfully transfered'
},
}
}
},
},
];

module.exports = tools;
2 changes: 1 addition & 1 deletion functions/placeOrder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function placeOrder(functionArgs) {
async function placeOrder(functionArgs) {
const {model, quantity} = functionArgs;
console.log('GPT -> called placeOrder function');

Expand Down
20 changes: 20 additions & 0 deletions functions/transferCall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require('dotenv').config();

const transferCall = async function (call) {

console.log('Transferring call', call.callSid);
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = require('twilio')(accountSid, authToken);

return await client.calls(call.callSid)
.update({twiml: `<Response><Dial>${process.env.TRANSFER_NUMBER}</Dial></Response>`})
.then(() => {
return 'The call was transferred successfully, say goodbye to the customer.';
})
.catch(() => {
return 'The call was not transferred successfully, advise customer to call back later.';
});
};

module.exports = transferCall;
5 changes: 5 additions & 0 deletions scripts/outbound-call.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
You can use this script to place an outbound call
to your own mobile phone.
*/

require('dotenv').config();

async function makeOutBoundCall() {
Expand Down
10 changes: 8 additions & 2 deletions services/gpt-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class GptService extends EventEmitter {
this.partialResponseIndex = 0;
}

// Add the callSid to the chat context in case
// ChatGPT decides to transfer the call.
setCallSid (callSid) {
this.userContext.push({ 'role': 'system', 'content': `callSid: ${callSid}` });
}

async completion(text, interactionCount, role = 'user', name = 'user') {
if (name != 'user') {
this.userContext.push({ 'role': role, 'name': name, 'content': text });
Expand Down Expand Up @@ -77,7 +83,7 @@ class GptService extends EventEmitter {
}

const functionToCall = availableFunctions[functionName];
let functionResponse = functionToCall(functionArgs);
let functionResponse = await functionToCall(functionArgs);

// Step 4: send the info on the function call and function response to GPT
this.userContext.push({
Expand All @@ -86,7 +92,7 @@ class GptService extends EventEmitter {
content: functionResponse,
});
// extend conversation with function response

console.log(this.userContext);
// call the completion function again but pass in the function response to have OpenAI generate a new assistant response
await this.completion(functionResponse, interactionCount, 'function', functionName);
} else {
Expand Down
1 change: 1 addition & 0 deletions services/stream-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class StreamService extends EventEmitter {
this.expectedAudioIndex = 0;
this.audioBuffer = {};
this.streamSid = '';
this.callSid = '';
}

setStreamSid (streamSid) {
Expand Down
31 changes: 31 additions & 0 deletions test/transferCall.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require('dotenv').config();
const setTimeout = require('timers/promises').setTimeout;
const transferCall = require('../functions/transferCall');

test('Expect transferCall to successfully redirect call', async () => {

async function makeOutBoundCall() {
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;

const client = require('twilio')(accountSid, authToken);

const sid = await client.calls
.create({
url: `https://${process.env.SERVER}/incoming`,
to: process.env.YOUR_NUMBER,
from: process.env.FROM_NUMBER
})
.then(call => call.sid);

return sid;
}

const callSid = await makeOutBoundCall();
console.log(callSid);
await setTimeout(10000);

const transferResult = await transferCall(callSid);

expect(transferResult).toBe('The call was transferred successfully');
}, 20000);

0 comments on commit bf0f91a

Please sign in to comment.