Combining AI telecommunication services with powerful geolocation data opens new possibilities for spatially aware travel applications, parcel tracking, and fleet management. This installment of SignalWire in Seconds places our AI agent into a wayfinding context, using a Node.js webserver to establish integrations with SignalWire’s phone number lookup REST API endpoint and the Google Maps Platform.
We'll cover the capabilities of a Call Flow Builder IVR equipped with location-based routing, as well as the major components of a SignalWire Markup Language (SWML) script designed to enable Carmen, an AI travel guide, to deliver personalized driving directions over the phone and via SMS.
This application consists of RESTful endpoints at each stage of the process for an uninterrupted data flow between APIs, our IVR, and our AI agent. The code samples are only a portion of the complete application: for the full code, view the official Github repository.
Contextual IVR routing with Call Flow Builder
We’ll begin by employing SignalWire’s phone number lookup REST API endpoint to determine the inbound caller's general location (based on the origin of their phone number).
Call Flow Builder helps developers navigate the complex intersection of telecommunication and geolocation, whether you’re charting a course for travel guides or emergency dispatch services to create a spatially-aware IVR using an intuitive drag-and-drop interface.
The Request node
The Request node is your avenue to accessing external data in your call flows. With support for HTTP methods like GET and POST, this node allows you to make requests to public URLs and use the response data during subsequent instructions.
Expanding base functionality with SWML expressions, such as ${call.from} and ${request_response_code}, the Request node allows developers to create complex workflows with little additional coding.
Phone number lookups
The call flow for this wayfinding application starts with a link to the first endpoint built for our Node.js server, `/start_pnl`. There, we’ll leverage SignalWire’s REST API to execute a phone number lookup of the inbound caller.
When a call enters our IVR, the inbound phone number is automatically passed from the IVR to the server using a custom header. The logic contained in the endpoint then normalizes the phone number format for the lookup and generates a query responsible for retrieving details like the caller’s city and state.
This data is logged to the console for real-time monitoring, and returned to the Call Flow Builder as a JSON response, informing downstream components of the caller's geographic context.
// SignalWire REST API endpoint Phone Number Lookup (PNL) | |
app.post('/start_pnl', async (req, res) => { | |
// Accept inbound phone number from the Call Flow Builder (CFB) | |
const from_number = req.headers['x-custom-header']; | |
console.log(`Inbound caller number transmitted by CFB: ${from_number}`); | |
// Normalize the phone number before passing it to the REST API | |
const newNumber = from_number.replace("+", "%2B"); | |
console.log(`From number converted for REST API query: ${newNumber}`); | |
// Execute the PNL | |
const client = await axios.get(`https://<YOUR-SPACE-URL>.signalwire.com/api/relay/rest/lookup/phone_number/${newNumber}?include=carrier`, | |
{ | |
auth: auth | |
}); | |
// Map the retrieved PNL info | |
const response = client.data; | |
const carrierCity = response.carrier.city; | |
const carrierState = response.carrier.state; | |
const carrierNum = response.e164; | |
console.log(`PNL caller data retrieved: | |
City: ${carrierCity} | |
State: ${carrierState} | |
E164 Number: ${carrierNum}` | |
); | |
// Return the retrieved PNL info to the CFB | |
res.status(200).json({ carrierCity, carrierState, carrierNum }); | |
}); |
The Conditions node
Acting like a JavaScript `if...else` statement, the Conditions node provides call flows with multiple routing scenarios. In this scenario, routing based on the origin state retrieved from the PNL furthers our concept of a travel agency.
Evaluating logical expressions, such as `${vars.request_response.carrierState === 'WY'}`, sends callers from Wyoming (WY) down a custom path, while defaulting to an "else" path for calls from other states. This workflow flexibility offers a streamlined call experience for users who prefer to reach a specialized agent.
The Execute SWML node
Finally, the Execute SWML node passes the inbound caller to an AI travel guide named Carmen by executing a remote SWML document which hosts her makeup. Critical variables collected earlier in the flow, like `${request_response.carrierState}`, are also transferred from the call flow to the target SWML script.
These variables remain accessible throughout the script’s execution, ensuring that Carmen can provide personalized guidance informed by context associated with the caller’s prior interaction with the IVR.
Designing an AI travel guide using SWML
AI voice agents like Carmen can be integrated into workflows that adapt to the unique needs of each business. Unlike rigid IVRs, AI agents can respond fluidly to individual situations, guiding travelers or tracking parcels through tailor-made telecom experiences.
Iterating on our lightweight server’s REST API integration, Carmen will make use of the Google Maps Platform to identify the caller’s point of interest (POI) and extract their current coordinates, delivering route-optimized driving directions via SMS, lightening the burden of navigation for busy users.
Prompt and conversation topography
Within the context of this wayfinding application, Carmen’s prompt is configured to emphasize her role as a knowledgeable and trustworthy AI travel guide, blending a clear identity with structured, step-by-step instructions.
Functional imperatives and response-reliant decision trees ensure that Carmen maintains an engaging tone while interacting with APIs to retrieve accurate geolocation data and route information. Additional fine-tuning parameters, like `temperature` and `top_p` balance creative dialogue with focused task execution, resulting in an AI agent that parses queries effectively and keeps conversations on topic.
Fetching points of interest and callers’ coordinates
The `fetch_poi` and `locate_caller` functions are key components of Carmen's wayfinding capabilities. These functions are designed to retrieve relevant POI candidates and determine the caller’s exact location based on the caller's input.
Configured as part of SignalWire's AI Gateway (SWAIG), these functions leverage arguments to precisely define the caller’s search. The `parameters` object, with properties such as `fetchPOI_placeName`, `caller_streetName`, serves to narrow the spatial scope of the Google Maps queries. Each parameter is described for clarity, reinforcing the AI’s ability to differentiate between a type of place (e.g., "gas station") and a specific geographic boundary.
SWML functions
Interfacing with our server via a webhook, SWML functions dynamically create query URLs by encoding the caller-provided parameters using the `enc:` prefix. This webhook sends a `GET` request to our similarly named server endpoints, retrieving a candidate POI and caller coordinates, among other details.
The gathered responses are then used to encourage a natural conversation with the caller, and allow Carmen to queue up proceeding functions, like `determine_route`.
"SWAIG": { | |
"functions": [ | |
{ | |
"function": "fetch_poi", | |
"description": "Function to retrieve a Point of Interest (POI) candidate relevant to the caller's query.", | |
"parameters": { | |
"type": "object", | |
"properties": { | |
"fetchPOI_placeName": { | |
"type": "string", | |
"description": "The proper name of a place, or the type of place, given by the caller." | |
}, | |
"fetchPOI_city": { | |
"type": "string", | |
"description": "A city name, given by the caller, that provides a spatial boundary to the query." | |
}, | |
"fetchPOI_state": { | |
"type": "string", | |
"description": "A state name, given by the caller, that provides a spatial boundary to the query." | |
}, | |
"fetchPOI_postalCode": { | |
"type": "string", | |
"descritption": "A postal code, given by the caller, that provides a spatial boundary to the query." | |
} | |
} | |
}, | |
"data_map": { | |
"webhooks": [ | |
{ | |
"url": "https://<Your-Server-Address>/fetch_poi?fetchPOI_placeName=${enc:args.fetchPOI_placeName}&fetchPOI_city=${enc:args.fetchPOI_city}&fetchPOI_state=${enc:args.fetchPOI_state}&fetchPOI_postalCode=${enc:args.fetchPOI_postalCode}", | |
"method": "GET", | |
"output": { | |
"response": "The nearest POI is... ${poiCandidate_name} at... ${poiCandidate_address}. The POI coordinates are... ${poiCandidate_latitude},${poiCandidate_longitude}", | |
"action": [ | |
{ | |
"toggle_functions": [ | |
{ | |
"function": "locate_caller", | |
"active": "true" | |
} | |
] | |
} | |
] | |
} | |
} | |
] | |
} | |
}, |
Server logic
On our server, the `/fetch_poi` and `/locate_caller` endpoints bridge Carmen's queries with the Google Maps Platform. Designed to align with their SWML functions, these endpoints accept the specified search parameters, which are then converted into acceptable structures for executing Places API and Geocode API requests.
From the APIs’ response data, critical details—such as the POI candidate’s formatted address or the caller’s latitude and longitude—are mapped into a structured JSON response and sent back to Carmen. If no candidates are found, the endpoint supplies an appropriate error message to the AI agent.
// Endpoint for gathering Point of Interest (POI) candidates | |
app.get("/fetch_poi", async (req, res) => { | |
// Accept incoming search parameters from the AI Agent | |
const { fetchPOI_placeName, fetchPOI_city, fetchPOI_state, fetchPOI_postalCode } = req.query; | |
console.log(`Query params transmitted by AI Agent: | |
Place Name: ${fetchPOI_placeName} | |
City: ${fetchPOI_city} | |
State: ${fetchPOI_state} | |
Postal Code: ${fetchPOI_postalCode}` | |
); | |
// Encode the query parameters for Google Maps API execution | |
const fullSearch = `${fetchPOI_placeName},${fetchPOI_city},${fetchPOI_state},${fetchPOI_postalCode}`; | |
const encodedSearch = encodeURIComponent(fullSearch); | |
// Construct the Google Maps API URL | |
const url = `https://maps.googleapis.com/maps/api/place/findplacefromtext/json?fields=name%2Cformatted_address%2Cgeometry&input=${encodedSearch}&inputtype=textquery&ipbias&key=${g_token}`; | |
console.log(`Constructed POI candidate URL: ${url}`); | |
// Send a GET request to the Google Maps API | |
const response = await axios.get(url); | |
const candidates = response.data.candidates; | |
if (candidates.length > 0) { | |
// Map the retrieved POI candidate info | |
const poiCandidate_name = candidates[0].name; | |
const poiCandidate_address = candidates[0].formatted_address; | |
const poiCandidate_latitude = candidates[0].geometry.location.lat; | |
const poiCandidate_longitude = candidates[0].geometry.location.lng; | |
console.log(`POI Candidate Retrieved: | |
Name: ${poiCandidate_name} | |
Address: ${poiCandidate_address} | |
Latitude: ${poiCandidate_latitude} | |
Longitude: ${poiCandidate_longitude}` | |
); | |
// Pass the POI candidate info back to the AI Agent | |
res.json({ poiCandidate_name, poiCandidate_address, poiCandidate_latitude, poiCandidate_longitude }); | |
} else { | |
console.log("No candidates found."); | |
res.status(404).json({ error: "No POI candidates found" }); | |
}; | |
}); |
Determining the caller’s route and dispatching SMS
Finally, we’ll retrieve driving directions from the caller’s given location to their desired POI using the Google Maps Directions API. With the `dispatch_route` function and the `/determine_route` server endpoint, raw geolocation data is transformed into travel guidance, delivering personalized driving directions via SMS.
The SWML function orchestrates the conversation’s flow of information from the caller to the Node.js server, where the endpoint interfaces with the Directions API to retrieve route details.
SWML Function
The `dispatch_route` function facilitates the process of determining a caller's route and dispatching turn-by-turn driving directions. With the parameters collected in each of the prior functions, a webhook request is made to the last of our server endpoints.
Upon the payload’s return, the webhook response instructs Carmen to provide the caller with a brief vocal summary of the compiled route information, and triggers an SMS action containing the trip’s details.
"data_map": { | |
"webhooks": [ | |
{ | |
"url": "https://<Your-Server-Address>/determine_route?poiCandidate_name=${enc:args.poiCandidate_name}&poiCandidate_latitude=${enc:args.poiCandidate_latitude}&poiCandidate_longitude=${enc:args.poiCandidate_longitude}&caller_latitude=${enc:args.caller_latitude}&caller_longitude=${enc:args.caller_longitude}&carrierNum=${enc:args.carrierNum}", | |
"method": "GET", | |
"output": { | |
"response": "The distance from the caller's... ${carrierNum} current location at... ${route_startAddress} to the location of... ${poiCandidate_name} at... ${route_endAddress} is... ${route_distance} with a drive time of... ${route_duration}", | |
"action": [ | |
{ | |
"SWML": { | |
"version": "1.0.0", | |
"sections": { | |
"main": [ | |
{ | |
"send_sms": { | |
"to_number": "${carrierNum}", | |
"from_number": "<Your-SW-Number>", | |
"body": "Thank you for sharing your wayfinding query with Carmen. The distance from your current location at ${route_startAddress} to the location of ${poiCandidate_name} at ${route_endAddress} is ${route_distance} with a drive time of ${route_duration}. Your directions are as follows: ${combinedInstructions}" | |
} | |
} | |
] | |
} | |
} | |
} | |
] | |
} | |
} | |
] | |
} |
Server Logic
The `/determine_route` endpoint processes the query transmitted by the AI agent, specifying coordinates representing both the caller and the POI candidate. The endpoint sends a request to the Google Maps Platform—this time aimed at the Directions API.
Following a calculation of the driving route between the caller's location and the POI, the API’s response is parsed to extract course information, like the origin and destination addresses, route distance, and expected travel time.
With SMS character count considerations in mind, turn-by-turn driving instructions are processed further, combining into a single string that enhances user readability and operational efficiency.
// Combine route directions into a single string | |
const route_steps = routes[0].legs[0].steps; | |
const combinedInstructions = route_steps | |
.map(step => { | |
const instruction = step.html_instructions.replace(/<[^>]+>/g, '').trim(); // Normalize html_instructions | |
const distance = step.distance.text; // Add distance text | |
return `${instruction} (${distance}).`; // Combine instruction and distance with formatting | |
}) | |
.join(' '); // Join all steps with a space |