-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ai demo initial commit * update npm package * testing postPrompt * add swaig functions * examples * update transfer function * fix fn response * add fallback * API updates * other attempts * fix * fix auth * const fix * clean up console logs * postprompt tweaks * postprompt fallbacks * supplementary materials * update post prompt url * typo * rename folder for consistency * comment and sanitize * move numbers out of the prompt * wrap comments * update readme * fix env vars number format * update guide link --------- Co-authored-by: Daniele Di Sarli <[email protected]>
- Loading branch information
Showing
7 changed files
with
1,887 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FROM node:alpine | ||
|
||
WORKDIR /app | ||
|
||
# Install app dependencies | ||
COPY package*.json ./ | ||
RUN npm install | ||
|
||
# Bundle app source | ||
COPY index.js . | ||
|
||
EXPOSE 3000 | ||
|
||
CMD [ "npm", "start" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Using SignalWire Conversational AI with Node.js | ||
|
||
Artificial Intelligence platforms are increasingly popular and constantly evolving tools that you can leverage when building a Voice application. SignalWire Conversational AI, currently integrated with OpenAI, was developed to simulate natural conversations. An AI agent allows businesses to automate routine tasks, handle high volumes of inquiries, provide instant support 24/7, and offer consistent and accurate information to customers. This example will implement a conversational AI agent with the [Compatibility SDK with Node.js](https://docs.signalwire.com/reference/compatibility-sdks/v3/#compatibility-rest-api-client-libraries-and-sdks-nodejs). Clone this repo to start testing and adapting SignalWire Conversational AI to meet your needs. | ||
|
||
## Setup Your Environment | ||
|
||
Copy the contents of `env.example` and save them in a new file called `.env`. Fill in your SignalWire credentials. If you need help finding them, check out our guide to [Navigating Your SignalWire Space](https://developer.signalwire.com/guides/navigating-your-space#api). The app will pull these environmental variables from the `.env` file automatically as long as the `.env` file is in the same parent directory. | ||
|
||
## Run Your Express Server | ||
|
||
This example serves our webhooks using an [Express](https://expressjs.com/en/starter/installing.html) server. After your environmental variables are set, you can install dependencies with `npm install` then start the Express server with `npm run start`. If you prefer to use Docker, build the image with `docker build -t aiagent .` and run it with `docker run -p 3000:3000 --env-file .env aiagent`. | ||
|
||
## Testing with Ngrok | ||
|
||
SignalWire requires that your webhooks be publicly accessible for them to be used with our services. So, we recommend using [Ngrok](https://ngrok.com/download) to provide an HTTPS URL for testing. In your Ngrok CLI, run `ngrok http 3000`, where 3000 is the port we set in our Express server. It will return a secure URL you can copy for the next step. You will also add this URL to the `.env` file as the `HOST_APP` to build webhook URLs. | ||
|
||
## Configure a Number to Accept Incoming Calls | ||
|
||
In your SignalWire Dashboard, you can purchase a phone number and edit its settings to direct calls to the Ngrok URL. The settings for your phone number of choice will look something like this: | ||
|
||
![phone number configuration in SignalWire dashboard](../SIP%20Voicemail%20with%20NodeJS/ngrok-webhook-config.png) | ||
|
||
With your server and Ngrok running, you should now be able to dial this number and test this example. | ||
|
||
## Code Walkthrough | ||
|
||
Read the full walkthrough in the [Conversational AI](https://developer.signalwire.com/guides/voice/conversational-ai-example) guide. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Project ID copied from the API Credentials in your SignalWire Space | ||
# Example: 7b98XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | ||
PROJECT_ID= | ||
|
||
# API token copied from the API Credentials in your SignalWire Space | ||
# Example: PTda745XXXXXXXXXXXXXXXXXXXXXXXXXXXXX | ||
API_TOKEN= | ||
|
||
# Your SignalWire Space URL copied from the API Credentials in your SignalWire Space | ||
# Example: spacename.signalwire.com | ||
SPACE_URL= | ||
|
||
# The URL of your deployed application, including the protocol extension | ||
# Example: https://example.ngrok.app | ||
HOST_APP= | ||
|
||
# 10-digit phone number for a sales department in the following format: | ||
# Example: 222-555-0110 | ||
SALES_NUMBER= | ||
|
||
# 10-digit phone number for a support department in the following format: | ||
# Example: 222-555-0110 | ||
SUPPORT_NUMBER= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import * as dotenv from "dotenv"; | ||
import fetch from "node-fetch"; | ||
dotenv.config(); | ||
import express from "express"; | ||
import { RestClient } from "@signalwire/compatibility-api"; | ||
const host = process.env.HOST_APP; | ||
|
||
const app = express(); | ||
app.use(express.urlencoded({ extended: true })); | ||
app.use(express.json()); | ||
|
||
const ADDRESS_BOOK = { | ||
sales: process.env.SALES_NUMBER, | ||
support: process.env.SUPPORT_NUMBER, | ||
}; | ||
|
||
// This route contains all of the information the AI agent will need for the | ||
// conversation. | ||
app.post("/", (req, res) => { | ||
const response = new RestClient.LaML.VoiceResponse(); | ||
const connect = response.connect(); | ||
// Initializing the AI agent | ||
const agent = connect.ai({ | ||
voice: "en-US-Neural2-D", | ||
}); | ||
agent.setPostPromptURL("/summary"); | ||
// Give the AI agent its role. Be as specific as possible. Do not include any | ||
// sensitive information because the AI agent will share it. | ||
agent.prompt( | ||
{ | ||
confidence: 0.4, | ||
frequencyPenalty: 0.3, | ||
}, | ||
`You are the CEO's assistant. | ||
Your job is to answer phone calls and collect messages or transfer the caller to a human agent. | ||
The CEO is not available now. Offer to take a message or transfer the caller to live agent. | ||
Request the caller's name. | ||
You are able to transfer a call to support or sales. | ||
Here is a list of departments that you can transfer the caller to: "Sales", "Support". Transfer the caller by using the appropriate function. | ||
If the caller would like to leave a message for the CEO, ask for their message. | ||
After collecting the message, do not wait for the user to end the conversation: say goodbye and hang up the call. | ||
Be sure to hang up the call at the end of every conversation.` | ||
); | ||
// Instruct the AI agent on what information to send about this call when it | ||
// is complete. | ||
agent.postPrompt(`Return a valid anonymous json object by replacing the uppercase placeholders in the following template with the caller's information. | ||
{"contact_name": "CONTACT_NAME", "message": "MESSAGE"}`); | ||
|
||
// SWAIG allows us to define custom external functions for the AI agent to | ||
// use. Name, purpose, argument, and webhook should be set as follows. | ||
const swaig = agent.swaig(); | ||
|
||
// Additional functions can be added using this same pattern. | ||
const transferFn = swaig.function({ name: "transfer" }); | ||
transferFn.setPurpose("use this when a request for a transfer is made"); | ||
transferFn.setArgument( | ||
'The destination to transfer to, in JSON format. Example: {"destination": "<destination>"}. Allowed values: "sales", "support".' | ||
); | ||
transferFn.setWebHookURL( | ||
`${host}/function?CallSid=${encodeURIComponent(req.body.CallSid)}` | ||
); | ||
|
||
console.log("AI agent handling new incoming call."); | ||
|
||
res.set("Content-Type", "text/xml"); | ||
res.send(response.toString()); | ||
}); | ||
|
||
// The AI agent will call this route if the caller asks requests an external | ||
// action that has been defined by a SWAIG function. | ||
app.post("/function", async (req, res) => { | ||
console.log(`AI agent has invoked the ${req.body.function} function`); | ||
const callSid = req.query.CallSid; | ||
|
||
if (req.body.function === "transfer") { | ||
// Get the destination name from the parameters (either "sales" or | ||
// "support") | ||
const destination = | ||
req.body.argument.parsed.destination ?? | ||
req.body.argument.parsed[0].destination; | ||
|
||
// Convert the destination name to a phone number | ||
const destNumber = ADDRESS_BOOK[destination.toLowerCase()]; | ||
if (!destNumber) { | ||
res.json({ response: "Unknown destination." }); | ||
return; | ||
} | ||
|
||
// Update the in-progress call to transfer to the requested number | ||
const result = await fetch( | ||
`https://${process.env.SPACE_URL}/api/laml/2010-04-01/Accounts/${ | ||
process.env.PROJECT_ID | ||
}/Calls/${encodeURIComponent(callSid)}`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
Authorization: | ||
"Basic " + | ||
Buffer.from( | ||
process.env.PROJECT_ID + ":" + process.env.API_TOKEN | ||
).toString("base64"), | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: new URLSearchParams({ | ||
Url: `${host}/transfer?number=${encodeURIComponent(destNumber)}`, | ||
Method: "POST", | ||
}), | ||
} | ||
); | ||
|
||
if (result.status !== 200) { | ||
res.json({ response: "Connection failed." }); | ||
return; | ||
} | ||
|
||
res.json({ response: "Connecting..." }); | ||
} else { | ||
res.json({ response: "Function not implemented" }); | ||
} | ||
}); | ||
|
||
// This is a helper route that will build XML and send it to the API to connect | ||
// the caller with a new number. | ||
app.post("/transfer", (req, res) => { | ||
const number = req.query.number; | ||
|
||
const response = new RestClient.LaML.VoiceResponse(); | ||
const dial = response.dial(); | ||
dial.number(number); | ||
|
||
console.log( | ||
`Transferring to ${number} with XML instructions: ` + response.toString() | ||
); | ||
|
||
res.set("Content-Type", "text/xml"); | ||
res.send(response.toString()); | ||
}); | ||
|
||
// This route receives final information from the AI agent in response to the | ||
// `postPrompt` that was set in the default route. | ||
app.post("/summary", (req, res) => { | ||
console.log("Call from " + req.body.caller_id_number); | ||
if (req.body.post_prompt_data.parsed) { | ||
console.log(req.body.post_prompt_data.parsed[0]); | ||
} else if (req.body.post_prompt_data) { | ||
console.log(req.body.post_prompt_data); | ||
} else { | ||
console.log("AI agent did not record a message."); | ||
} | ||
}); | ||
|
||
app.listen(process.env.PORT || 3000, () => { | ||
console.log("Server listening"); | ||
}); |
Oops, something went wrong.