Skip to content

Commit

Permalink
Merge pull request #73 from beckn/add_roadblock_exception
Browse files Browse the repository at this point in the history
Added api for location based exceptions
  • Loading branch information
mayurvir authored Apr 8, 2024
2 parents 10463d2 + c584cb0 commit fb34607
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 25 deletions.
2 changes: 1 addition & 1 deletion config/openai.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"SUPPORTED_ACTIONS": [
{ "action": "get_routes", "description": "If the user has requested for routes for a travel plan between two places or asked to plan a trip." },
{ "action": "get_routes", "description": "If the user has requested for routes for a travel plan between two places or asked to plan a trip. If the assistant has suggested to re-route in the last message and asked user to share current location, it should be a get_routes." },
{ "action": "select_route", "description": "If the user selects one of the routes from the routes shared by the assistant." },
{ "action": "search", "description": "If the user clearly indicates to perform a search for a specific product. Sample instructions : 'find a hotel', 'find an ev charger', 'find tickets'" },
{ "action": "select", "description": "If the user likes or selects any item, this action should be used. This action can only be called if a search has been called before." },
Expand Down
2 changes: 1 addition & 1 deletion controllers/Bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async function process_text(req, res) {
response.formatted = 'Session & profile cleared! You can start a new session now.';
}
else if(ai.action?.action === 'get_routes'){
const routes = await mapService.generate_routes(message, session.text);
const routes = await mapService.generate_routes(message, session.text, session.avoid_point|| []);
const formatting_response = await ai.format_response(routes.data?.routes_formatted || routes.errors, [{ role: 'user', content: message },...session.text]);
response.formatted = formatting_response.message;
session.routes = routes.data?.routes || session.routes;
Expand Down
65 changes: 54 additions & 11 deletions controllers/ControlCenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
CANCEL_BOOKING_MESSAGE,
TOURISM_STRAPI_URL
} from '../utils/constants.js'
import DBService from '../services/DBService.js'
import MapsService from '../services/MapService.js'

const action = new Actions()

Expand Down Expand Up @@ -46,7 +48,7 @@ export const cancelBooking = async (req, res) => {
}
return res.status(200).send({ message: `Notification ${statusMessage}`, status:true })
}

return res.status(200).send({ message: 'Cancel Booking Failed', status:false })
} catch (error) {
logger.error(error.message)
Expand All @@ -65,7 +67,7 @@ export const updateCatalog = async (req, res) => {
},
},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})
const notifyResponse = await action.send_message(userNo, messageBody)

if(!notifyResponse || notifyResponse.deliveryStatus === "failed"){
throw new Error('Notification Failed')
}
Expand All @@ -84,14 +86,55 @@ export const notify = async (req, res) => {
const sendWhatsappNotificationResponse = await action.send_message(
userNo,
messageBody
)
if(sendWhatsappNotificationResponse.deliveryStatus === "failed"){
return res.status(400).json({...sendWhatsappNotificationResponse, status:false})
)
if(sendWhatsappNotificationResponse.deliveryStatus === "failed"){
return res.status(400).json({...sendWhatsappNotificationResponse, status:false})
}
sendWhatsappNotificationResponse.deliveryStatus = 'delivered'
return res.status(200).json({...sendWhatsappNotificationResponse, status:true})
} catch (error) {
logger.error(error.message)
return res.status(400).send({ message: error.message, status:false })
}
sendWhatsappNotificationResponse.deliveryStatus = 'delivered'
return res.status(200).json({...sendWhatsappNotificationResponse, status:true})
} catch (error) {
logger.error(error.message)
return res.status(400).send({ message: error.message, status:false })
}
}

export const triggerExceptionOnLocation = async (req, res) => {
const {point, message} = req.body; // needs to be an array with 2 numbers [lat, long]
const db = new DBService();
const mapService = new MapsService();

if(point && message){
// get all active sessions
const sessions = await db.get_all_sessions();
logger.info(`Got ${sessions.length} sessions.`)

// check if point exists on route
for(let session of sessions){
const selected_route = session.data.selected_route;
if(selected_route?.overview_polyline?.points) {
const status = await mapService.checkGpsOnPolygon(point, selected_route?.overview_polyline?.points)

logger.info(`Status of gps point ${JSON.stringify(point)} on route ${selected_route.summary} is ${status}`)
// send whatsapp and add to context
if(status){
try{
const reply_message = `${message}. Please share your current location and I'll try to find some alternate routes?`
await action.send_message(session.key, reply_message);

// update session
session.data.avoid_point = point;
if(!session.data.text) session.data.text=[]
session.data.text.push({role: 'assistant', content: reply_message});

await db.update_session(session.key, session.data);
}
catch(e){
logger.error(e);
}
}
}
}
res.send("Triggered!")
}
else res.status(400).send('Point and message are required in the body.')
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"license": "ISC",
"dependencies": {
"@googlemaps/google-maps-services-js": "^3.3.42",
"@mapbox/polyline": "^1.2.1",
"axios": "^1.6.7",
"body-parser": "^1.20.2",
"chai": "^5.0.0",
Expand Down
4 changes: 3 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import DBService from './services/DBService.js'
import {
cancelBooking,
updateCatalog,
notify
notify,
triggerExceptionOnLocation
} from './controllers/ControlCenter.js'
const app = express()
app.use(cors())
Expand All @@ -25,6 +26,7 @@ app.post('/webhook', messageController.process_text)
app.post('/notify', notify)
app.post('/cancel-booking', cancelBooking)
app.post('/update-catalog', updateCatalog)
app.post('/trigger-exception', triggerExceptionOnLocation)


// Reset all sessions
Expand Down
32 changes: 32 additions & 0 deletions services/DBService.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,38 @@ class DBService {
logger.info(response)
return response
}

async get_all_sessions(){
const sessions = [];
let cursor = '0';

try{
do {
// Use the SCAN command to iteratively retrieve keys that match the "session:*" pattern.
const reply = await this.redisClient.scan(cursor, {
MATCH: '*',
COUNT: 100, // Adjust based on your expected load
});

cursor = reply.cursor;
const keys = reply.keys;

// For each key, get the session data and add it to the sessions array.
for (let key of keys) {
const sessionData = await this.redisClient.get(key);
sessions.push({
key,
data: JSON.parse(sessionData),
});
}
} while (cursor !== 0);
}
catch(e){
logger.error(e);
}

return sessions;
}
}

export default DBService;
93 changes: 84 additions & 9 deletions services/MapService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {Client} from "@googlemaps/google-maps-services-js";
import logger from '../utils/logger.js'
import AI from './AI.js'
const ai = new AI();
import polyline from '@mapbox/polyline';


class MapsService {
constructor() {
this.client = new Client({});
}

async getRoutes(source, destination) {
async getRoutes(source, destination, avoidPoint=[]) {
try {
const response = await this.client.directions({
params: {
Expand All @@ -19,7 +20,17 @@ class MapsService {
alternatives: true
}
});
return response.data.routes;
let routes= [];
for(const route of response.data.routes){
const status = await this.checkGpsOnPolygon(avoidPoint, route.overview_polyline.points)
if(!status) routes.push(route)
}

const path = this.get_static_image_path(routes);
logger.info(`Static image path for routes: ${path}`);


return routes;
} catch (error) {
logger.error(error);
return [];
Expand Down Expand Up @@ -104,14 +115,10 @@ class MapsService {
}
})

let polygon_path = '';
routes.forEach((route, index) => {
polygon_path+=`&path=color:${this.get_random_color()}|weight:${5-index}|enc:${route.overview_polyline.points}`;
})
// print path
const path = this.get_static_image_path(routes)
logger.info(`Route image path : ${path}`)

const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300${polygon_path}&key=${process.env.GOOGLE_MAPS_API_KEY}`;
logger.info(`Map url :${route_image}`)

response.data.routes_formatted = {
"description": `these are the various routes that you can take. Which one would you like to select:`,
"routes": response.data.routes.map((route, index) => `Route ${index+1}: ${route.summary}`)
Expand All @@ -123,6 +130,74 @@ class MapsService {
// logger.info(`Generated routes response : ${JSON.stringify(response, null, 2)}`);
return response;
}

/**
* Check if a GPS point is on a polyline
*
* @param {Array<Number>} point - The GPS point to check, in [latitude, longitude] format.
* @param {String} encodedPolyline - The encoded overview polyline from Google Maps Directions API.
* @param {Number} tolerance - The maximum distance (in meters) for a point to be considered on the polyline.
* @returns {Boolean} true if the point is on the polyline within the specified tolerance, false otherwise.
*/
async checkGpsOnPolygon(point, encodedPolyline, tolerance = 500){
// Decode the polyline to get the array of points
const polylinePoints = polyline.decode(encodedPolyline);

// Check each segment of the polyline
for (let i = 0; i < polylinePoints.length - 1; i++) {
const start = polylinePoints[i];
const end = polylinePoints[i + 1];

if (this.isPointNearLineSegment(point, start, end, tolerance)) {
return true;
}
}

return false;
}

isPointNearLineSegment(point, start, end, tolerance) {
// Convert degrees to radians
const degToRad = deg => (deg * Math.PI) / 180;

// Earth radius in meters
const R = 6371000;

// Point latitude and longitude in radians
const pointLatRad = degToRad(point[0]);
const pointLonRad = degToRad(point[1]);

// Start point latitude and longitude in radians
const startLatRad = degToRad(start[0]);
const startLonRad = degToRad(start[1]);

// End point latitude and longitude in radians
const endLatRad = degToRad(end[0]);
const endLonRad = degToRad(end[1]);

// Using the 'cross-track distance' formula
const delta13 = Math.acos(Math.sin(startLatRad) * Math.sin(pointLatRad) +
Math.cos(startLatRad) * Math.cos(pointLatRad) * Math.cos(pointLonRad - startLonRad)) * R;
const theta13 = Math.atan2(Math.sin(pointLonRad - startLonRad) * Math.cos(pointLatRad),
Math.cos(startLatRad) * Math.sin(pointLatRad) - Math.sin(startLatRad) * Math.cos(pointLatRad) * Math.cos(pointLonRad - startLonRad));
const theta12 = Math.atan2(Math.sin(endLonRad - startLonRad) * Math.cos(endLatRad),
Math.cos(startLatRad) * Math.sin(endLatRad) - Math.sin(startLatRad) * Math.cos(endLatRad) * Math.cos(endLonRad - startLonRad));

const deltaXt = Math.asin(Math.sin(delta13 / R) * Math.sin(theta13 - theta12)) * R;

return Math.abs(deltaXt) < tolerance;
}

get_static_image_path(routes){
let polygon_path = '';
routes.forEach((route, index) => {
polygon_path+=`&path=color:${this.get_random_color()}|weight:${5-index}|enc:${route.overview_polyline.points}`;
})

const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300${polygon_path}&key=${process.env.GOOGLE_MAPS_API_KEY}`;
return route_image;

}
}

export default MapsService;
2 changes: 1 addition & 1 deletion tests/apis/agent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ describe('test cases for generating routes', ()=>{
})
})

describe.only('test cases for generating routes and selecting a route', ()=>{
describe('test cases for generating routes and selecting a route', ()=>{

it('Should share routes when asked to share routes.', async () => {
const ask = "Can you get routes from Denver to Yellowstone national park?";
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/controllers/controlCenter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,33 @@ describe('API tests for /update-catalog endpoint for an end to end Notify Messag
expect(response._body.status).equal(true)
expect(response._body.message).equal('Catalog Updated')
})
})

describe('API tests for triggering a roadblock', ()=>{
it('Should trigger a roadblock on a selected route', async ()=>{
const ask1 = "Can you get routes from Denver to Yellowstone national park?";
await request(app).post('/webhook').send({
"From": process.env.TEST_RECEPIENT_NUMBER,
"Body": ask1
})

const ask2 = "Lets select the first route.";
await request(app).post('/webhook').send({
"From": process.env.TEST_RECEPIENT_NUMBER,
"Body": ask2
})

const response = await request(app).post('/trigger-exception').send({
"point":[39.7408351, -104.9874105],
"message": "There is a roadblock on your selected route due to an accident!"
})

const ask3 = "I'm near Glendo";
await request(app).post('/webhook').send({
"From": process.env.TEST_RECEPIENT_NUMBER,
"Body": ask3
})

expect(response.status).equal(200)
})
})
20 changes: 19 additions & 1 deletion tests/unit/services/maps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,30 @@ describe('Should test the map service', () => {

it('should return GPS coordinates for a valid address', async () => {

const gpsCoordinates = await mapService.lookupGps('1600 Amphitheatre Parkway, Mountain View, CA');
const gpsCoordinates = await mapService.lookupGps('Yellowstone national park');
expect(gpsCoordinates).to.be.an('object');
expect(gpsCoordinates).to.have.property('lat');
expect(gpsCoordinates).to.have.property('lng');
})

it('Sould return true if a given gps location falls on a selected polygon', async()=>{

const source ='37.422391,-122.084845';
const destination = '37.411991,-122.079414';

const point = [37.422391, -122.084845];
let routes = await mapService.getRoutes(source, destination);

const status = await mapService.checkGpsOnPolygon(point, routes[0].overview_polyline.points);
expect(status).to.be.true;
})

it('Should return path avoiding certail points', async ()=>{
const source ='39.7392358,-104.990251';
const destination = '44.427963, -110.588455';
const pointBeforeCasper = [42.839531, -106.136404];
await mapService.getRoutes(source, destination, pointBeforeCasper);
})

});

0 comments on commit fb34607

Please sign in to comment.