diff --git a/config/openai.json b/config/openai.json index 9d0d350..ec28386 100644 --- a/config/openai.json +++ b/config/openai.json @@ -1,5 +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": "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." }, { "action": "init", "description": "If the user wants to place an order after search and select and has shared the billing details. This action can only be called if a select has been called before." }, diff --git a/controllers/Bot.js b/controllers/Bot.js index 8c76dd0..c19d10f 100644 --- a/controllers/Bot.js +++ b/controllers/Bot.js @@ -3,6 +3,8 @@ import AI from '../services/AI.js' import DBService from '../services/DBService.js' import logger from '../utils/logger.js' import { v4 as uuidv4 } from 'uuid' +import MapsService from '../services/MapService.js' +const mapService = new MapsService() const actionsService = new ActionsService() const db = new DBService(); @@ -78,7 +80,7 @@ async function process_text(req, res) { // inputs let message = req.body.Body const sender = req.body.From - const format = req.headers['content-type'] || 'text/xml'; + const format = (req?.headers && req.headers['content-type']) || 'text/xml'; const raw_yn = req.body.raw_yn || false; let response= { @@ -95,7 +97,9 @@ async function process_text(req, res) { formatted: [] }, bookings: [], - active_transaction: null + active_transaction: null, + routes:[], + selected_route:null } // Update lat, long @@ -168,6 +172,16 @@ async function process_text(req, res) { session = EMPTY_SESSION; 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 formatting_response = await ai.format_response(routes.data?.routes_formatted || routes.errors, [...session.actions.formatted, { role: 'user', content: message },...session.text]); + response.formatted = formatting_response.message; + session.routes = routes.data?.routes || session.routes; + logger.info(`AI response: ${response.formatted}`); + + session.text.push({ role: 'user', content: message }); + session.text.push({ role: 'assistant', content: response.formatted }); + } else if(ai.action?.action == null) { // get ai response diff --git a/services/AI.js b/services/AI.js index 44135f3..453625f 100644 --- a/services/AI.js +++ b/services/AI.js @@ -549,10 +549,11 @@ class AI { return bookings_updated; } - async get_details_by_description(message, desired_output){ + async get_details_by_description(message, context=[], desired_output){ const openai_messages = [ - { role: 'system', content: `Your job is to analyse the given user input and extract details in the json format given : ${JSON.stringify(desired_output)}` }, + { role: 'system', content: `Your job is to analyse the given user input and extract details in the json format given : ${desired_output}` }, + ...context, { role: 'user', content: message } ] diff --git a/services/MapService.js b/services/MapService.js index bea70d8..3b0a56c 100644 --- a/services/MapService.js +++ b/services/MapService.js @@ -1,5 +1,7 @@ import {Client} from "@googlemaps/google-maps-services-js"; import logger from '../utils/logger.js' +import AI from './AI.js' +const ai = new AI(); class MapsService { @@ -53,6 +55,57 @@ class MapsService { return encodeURIComponent(color); } + + async generate_routes(message, context=[]) { + let response = { + status:false, + data: {}, + errors: [] + }; + + // identify source and destination + const format = { + 'source': 'SOURCE_LOCATION', + 'destination': 'DESTINATION_LOCATION' + } + + const details = await ai.get_details_by_description(message, context, JSON.stringify(format)); + logger.info(JSON.stringify(details, null, 2)); + if(!details.source || !details.destination) { + if (!details.source ) { + response.errors.push("Can you please specify the source location?"); + } + if (!details.destination) { + response.errors.push("Can you please specify the destination location?"); + } + } + else{ + // Get gps for source and destination + const source_gps = await this.lookupGps(details.source); + const destination_gps = await this.lookupGps(details.destination); + + if(!source_gps || !destination_gps) { + if(!source_gps) { + response.errors.push("Can you please specify the source location?"); + } + if(!destination_gps) { + response.errors.push("Can you please specify the destination location?"); + } + } + else{ + // generate routes + response.data.routes = await this.getRoutes(source_gps, destination_gps); + 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}`) + } + response.status = true; + } + } + + logger.info(`Generated routes response : ${JSON.stringify(response, null, 2)}`); + return response; + } } export default MapsService; diff --git a/tests/unit/controllers/bot.test.js b/tests/unit/controllers/bot.test.js index 80a02a3..3b3247e 100644 --- a/tests/unit/controllers/bot.test.js +++ b/tests/unit/controllers/bot.test.js @@ -8,6 +8,8 @@ const expect = chai.expect const mapService = new MapsService() const ai = new AI(); const actionsService = new ActionService() +import request from 'supertest' +import app from '../../../server.js' describe.only('Test cases for AI', () => { it('Should return message with location polygon', async () => { @@ -68,7 +70,7 @@ describe.only('Test cases for Google maps', () => { 'destination': 'DESTINATION_LOCATION' } - const details = await ai.get_details_by_description(ask, format); + const details = await ai.get_details_by_description(ask, [], JSON.stringify(format)); expect(details).to.have.property('source'); expect(details).to.have.property('destination'); @@ -150,6 +152,49 @@ describe.only('Test cases for Google maps', () => { logger.info(`route_image: ${route_image}`); expect(routes).to.be.an('array'); }) -}) + it('It should share a set of routes when given an instruction.', async () => { + const ask = "Can you plan a trip from Denver to Yellowstone national park?"; + + // generate routes + const routesResponse = await mapService.generate_routes(ask); + expect(routesResponse).to.have.property('status'); + expect(routesResponse).to.have.property('data'); + expect(routesResponse.status).to.be.true; + expect(routesResponse.data.routes).to.be.an('array').that.is.not.empty; + }) + + it('It should ask for details when given an incomplete instruction.', async () => { + const ask = "Can you plan a trip to Yellowstone national park?"; + + // generate routes + const routesResponse = await mapService.generate_routes(ask); + expect(routesResponse).to.have.property('status'); + expect(routesResponse).to.have.property('errors'); + expect(routesResponse.status).to.be.false; + expect(routesResponse.errors).to.be.an('array').that.is.not.empty; + }) + + it('Should share routes when asked to share routes.', async () => { + const ask = "Can you get routes from Denver to Yellowstone national park?"; + const response = await request(app).post('/webhook').send({ + "From": process.env.TEST_RECEPIENT_NUMBER, + "Body": ask + }) + logger.info(JSON.stringify(response.text, null, 2)); + expect(response.status).to.be.eq(200) + + }) + + it('Should come back asking for more details.', async () => { + const ask = "Can you get routes to Yellowstone national park?"; + const response = await request(app).post('/webhook').send({ + "From": process.env.TEST_RECEPIENT_NUMBER, + "Body": ask + }) + logger.info(response.text); + expect(response.status).to.be.eq(200) + + }) +})