From b9507286a9df97502b29f067358bb68156ba4564 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Sun, 14 Apr 2024 16:32:33 +0530 Subject: [PATCH 01/11] - modified getRoutes function - Added one function to get response or perform an action using function calling --- config/GPT/tools.js | 43 +++++++++ controllers/Agent.js | 91 +++++++++++++++++++ services/AI.js | 125 +++++++++++++++++++++++++++ services/Actions.js | 1 - services/MapService.js | 63 ++++++-------- tests/unit/controllers/agent.test.js | 33 +++++++ tests/unit/controllers/bot.test.js | 14 +-- tests/unit/services/maps.test.js | 15 +++- 8 files changed, 339 insertions(+), 46 deletions(-) create mode 100644 config/GPT/tools.js create mode 100644 controllers/Agent.js create mode 100644 tests/unit/controllers/agent.test.js diff --git a/config/GPT/tools.js b/config/GPT/tools.js new file mode 100644 index 0000000..6986ced --- /dev/null +++ b/config/GPT/tools.js @@ -0,0 +1,43 @@ +export const TOOLS = [ + { + type: "function", + function: { + name: "get_routes", + description: "This function should only be used if the user specifically asks to get routes between a source and destination using google maps.", + parameters: { + type: "object", + properties: { + source:{ + type:"string", + description: "Source location in the format 'latitude,longitude' or 'text' from which route is to be fetched" + }, + destination:{ + type:"string", + description: "Destination location in the format 'latitude,longitude' or 'text' to which route is to be fetched" + } + } + } + } + }, + { + type: "function", + function: { + name: "perform_beckn_action", + description: "If the user is trying to search a product, select an item or add to cart, initialize an order or confirm on order.", + parameters: { + type: "object", + properties: { + action : { + type: "string", + description: "Action for which payload is to be fetched", + enum: ["search", "select", "init", "confirm"] + }, + instruction: { + type: "string", + description: "Complete instructions about the action to be performed." + } + } + } + } + } +]; \ No newline at end of file diff --git a/controllers/Agent.js b/controllers/Agent.js new file mode 100644 index 0000000..cca6ed2 --- /dev/null +++ b/controllers/Agent.js @@ -0,0 +1,91 @@ +import MapsService from "../services/MapService.js"; +import { TOOLS } from "../config/GPT/tools.js"; +import logger from "../utils/logger.js"; +import OpenAI from "openai"; +import AI from "../services/AI.js"; +const map = new MapsService(); +const ai = new AI(); + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_AI_KEY, +}) + +const AVAILABLE_TOOLS = { + get_routes: map.getRoutes.bind(map), + perform_beckn_action: ai.performAction.bind(ai), +}; + +async function getResponse(req, res) { + const { From, Body } = req.body + + // get session + + // get answer from AI + + // save session + + res.send(response) +} + + + + + +async function getResponseFromOpenAI(messages){ + const context = [ + {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, + {role: 'assistant', content : "You can only share results immediately, so you should never say that you will do something in the future. "}, + {role: 'assistant', content : "If the last tool call did not produce any useful response, you should convey that directly."}, + {role: 'assistant', content : "Your tone should be polite and helpful. "}, + + ] + try{ + const gpt_response = await openai.chat.completions.create({ + model: process.env.OPENAI_MODEL_ID, + messages: [...context, ...messages], + tools: TOOLS, + tool_choice: "auto", + }); + let responseMessage = gpt_response.choices[0].message; + + // check for tool calls + const toolCalls = responseMessage.tool_calls; + if (toolCalls) { + logger.info("Tool calls found in response, proceeding..."); + + + messages.push(responseMessage); + + for (let tool of toolCalls) { + const parameters = JSON.parse(tool.function.arguments); + const functionToCall = AVAILABLE_TOOLS[tool.function.name]; + if (functionToCall) { + const response = await functionToCall(parameters); + + messages.push({ + tool_call_id: tool.id, + role: "tool", + name: functionToCall, + content: JSON.stringify(response), + }); + + // call again to get the response + responseMessage = await getResponseFromOpenAI(messages); + } + } + } + + return responseMessage; + + } + catch(e){ + logger.error(e); + return false; + } +} + + +export default { + getResponse, + getResponseFromOpenAI +} \ No newline at end of file diff --git a/services/AI.js b/services/AI.js index d07641e..3fb06e5 100644 --- a/services/AI.js +++ b/services/AI.js @@ -5,6 +5,11 @@ import yaml from 'js-yaml' import { v4 as uuidv4 } from 'uuid' import search from '../schemas/jsons/search.js'; import actions from '../schemas/jsons/actions.js'; +import Actions from './Actions.js'; + +const SCHEMAS = { + search: search +} const openai = new OpenAI({ apiKey: process.env.OPENAI_AI_KEY, @@ -18,6 +23,7 @@ class AI { this.context = []; this.action = null; this.bookings = []; + this.actionService = new Actions(); } async get_beckn_action_from_text(instruction, context=[], last_action=null){ @@ -557,6 +563,125 @@ class AI { return {}; } } + + async performAction({action, instruction}){ + logger.info(`performAction() : ${action}, ${instruction}`); + let response = { + status: false, + data: null, + message: null + } + + try{ + // get context + const context = await this.get_context_by_action(action, instruction); + + // get message + const message = await this.get_message_by_action(action, instruction); + + // call API + const url = `${context.base_url}/${action}`; + const api_response = await this.actionService.call_api(url, 'POST', {context: context, message: message}); + response={ + status: true, + data: api_response.data + } + } + catch(e){ + logger.error(e); + response.message = e.message; + } + + return response; + } + + async get_context_by_action(action, instruction){ + + const desired_structure = { + domain:`DOMAIN_AS_PER_REGISTRY_AND_INSTRUCTION_GIVEN_BY_USER` + } + + let last_action_context=[]; + if(action!='search'){ + desired_structure.bpp_id = ``; + desired_structure.bpp_uri = ``; + } + + let response = { + message_id : uuidv4(), + transaction_id: uuidv4(), + base_url: registry_config[0].url, + bap_id: registry_config[0].bap_subscriber_id, + bap_uri: registry_config[0].bap_subscriber_url, + action: action, + version: registry_config[0].version, + + } + + const openai_messages = [ + { role: 'system', content: `Your job is to analyse the given instruction, registry details and generate a config json in the following structure : ${JSON.stringify(desired_structure)}` }, + { role: 'system', content: `Registry : ${JSON.stringify(registry_config)}` }, + { role: 'system', content: `Instruction : ${instruction}` }, + ...last_action_context, + ] + + try { + const completion = await openai.chat.completions.create({ + messages: openai_messages, + model: process.env.OPENAI_MODEL_ID, + temperature: 0, + response_format: { type: 'json_object' }, + }) + let gpt_response = JSON.parse(completion.choices[0].message.content) + response = {...response, ...gpt_response}; + logger.info(`Got context from instruction : ${JSON.stringify(response)}`); + return response; + } catch (e) { + logger.error(e) + return {} + } + } + + async get_message_by_action(action, instruction) { + logger.info(`get_message_by_action() : ${action}, ${instruction}`) + + const messages = [ + { role: "user", content: instruction } + ]; + + const schema = SCHEMAS[action]; + + const tools = [ + { + type: "function", + function: { + name: "get_message", + description: "Get the correct message object based on user instructions", + parameters: schema + } + } + ]; + + try{ + // Assuming you have a function to abstract the API call + const response = await openai.chat.completions.create({ + model: 'gpt-4-0125-preview', + messages: messages, + tools: tools, + tool_choice: "auto", // auto is default, but we'll be explicit + }); + const responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; + logger.info(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); + + return responseMessage + } + catch(e){ + logger.error(e); + return null; + } + } + + } export default AI; \ No newline at end of file diff --git a/services/Actions.js b/services/Actions.js index 2dedc66..a1d5813 100644 --- a/services/Actions.js +++ b/services/Actions.js @@ -18,7 +18,6 @@ const rootPath = path.resolve(__dirname, './'); class Actions { constructor() { - this.ai = new AI() this.context = []; } diff --git a/services/MapService.js b/services/MapService.js index 22a04f9..085a19a 100644 --- a/services/MapService.js +++ b/services/MapService.js @@ -11,7 +11,14 @@ class MapsService { this.client = new Client({}); } - async getRoutes(source, destination, avoidPoint=[]) { + /** + * + * @param {*} source | format : [latitude, longitude], "latitude,longitude" + * @param {*} destination | format : "latitude,longitude" + * @param {*} avoidPoint | format : [[latitude, longitude], [latitude, longitude], ...] + * @returns + */ + async getRoutes({source, destination, avoidPoint=[]}) { try { const response = await this.client.directions({ params: { @@ -30,9 +37,8 @@ class MapsService { const path = this.get_static_image_path(routes); logger.info(`Static image path for routes: ${path}`); - - return routes; + return routes.map(route => route.summary); } catch (error) { logger.error(error); return []; @@ -93,40 +99,27 @@ class MapsService { } } 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(get_text_by_key('missing_source')); - } - if(!destination_gps) { - response.errors.push(get_text_by_key('missing_destination')); + + // generate routes + const routes = await this.getRoutes({...details, avoidPoint: avoid_point}); + response.data.routes = routes.map(route=>{ + return { + overview_polyline: route.overview_polyline, + summary: route.summary, + source_gps: details.source, + destination_gps: details.destination } + }) + + // print path + const path = this.get_static_image_path(routes) + logger.info(`Route image path : ${path}`) + + response.data.routes_formatted = { + "description": get_text_by_key('route_list_description'), + "routes": response.data.routes.map((route, index) => `Route ${index+1}: ${route.summary}\n`) } - else{ - // generate routes - const routes = await this.getRoutes(`${source_gps.lat},${source_gps.lng}`, `${destination_gps.lat},${destination_gps.lng}`, avoid_point); - response.data.routes = routes.map(route=>{ - return { - overview_polyline: route.overview_polyline, - summary: route.summary, - source_gps: source_gps, - destination_gps: destination_gps - } - }) - - // print path - const path = this.get_static_image_path(routes) - logger.info(`Route image path : ${path}`) - - response.data.routes_formatted = { - "description": get_text_by_key('route_list_description'), - "routes": response.data.routes.map((route, index) => `Route ${index+1}: ${route.summary}\n`) - } - response.status = true; - } + response.status = true; } // logger.info(`Generated routes response : ${JSON.stringify(response, null, 2)}`); diff --git a/tests/unit/controllers/agent.test.js b/tests/unit/controllers/agent.test.js new file mode 100644 index 0000000..2026d4e --- /dev/null +++ b/tests/unit/controllers/agent.test.js @@ -0,0 +1,33 @@ +import { describe, it} from 'mocha' +import * as chai from 'chai' +import agent from '../../../controllers/Agent.js'; +const expect = chai.expect + +describe.only('API tests for getResponseFromOpenAI() function', () => { + it('Should return a string message for a common question', async ()=>{ + let messages = [ + { role: 'user', content: "What is the capital of India?" }, + ]; + + const response = await agent.getResponseFromOpenAI(messages); + expect(response.content).to.be.a('string'); + expect(response.content).to.contain('New Delhi'); + }) + + it('Should return routes when asked for routes', async ()=>{ + let messages = [ + { role: 'user', content: "Can you get me routes from Delhi to Mumbai?"}, + ]; + const response = await agent.getResponseFromOpenAI(messages); + expect(response.content).to.be.a('string'); + expect(response.content).to.contain('NH 48'); + }) + + it('Should be able to search for items on network', async ()=>{ + let messages = [ + { role: 'user', content: "Can you find some ev chargers near Denver?"}, + ]; + const response = await agent.getResponseFromOpenAI(messages); + expect(response.content).to.be.a('string'); + }) +}) \ No newline at end of file diff --git a/tests/unit/controllers/bot.test.js b/tests/unit/controllers/bot.test.js index 1561481..f2c0a58 100644 --- a/tests/unit/controllers/bot.test.js +++ b/tests/unit/controllers/bot.test.js @@ -17,7 +17,7 @@ describe('Test cases for AI', () => { const destination_gps = await mapService.lookupGps('Yellowstone national park'); // generate routes - const routes = await mapService.getRoutes(source_gps, destination_gps); + const routes = await mapService.getRoutes({source: source_gps, destination: destination_gps}); const context=[ { role : 'user', content: `Selected route polygon is : ${routes[0].overview_polyline.points}`} @@ -41,7 +41,7 @@ describe('Test cases for Google maps', () => { const source ='37.422391,-122.084845'; const destination = '37.411991,-122.079414'; - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); const selected_route = 0; const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300&path=enc:${routes[selected_route].overview_polyline.points}&key=${process.env.GOOGLE_MAPS_API_KEY}`; logger.info(`route_image: ${route_image}`); @@ -81,7 +81,7 @@ describe('Test cases for Google maps', () => { const destination_gps = await mapService.lookupGps(details.destination); // generate routes - const routes = await mapService.getRoutes(source_gps, destination_gps); + const routes = await mapService.getRoutes({source: source_gps, destination: destination_gps}); // Selected route const selected_route = 0; @@ -101,7 +101,7 @@ describe('Test cases for Google maps', () => { const source ='37.422391,-122.084845'; const destination = '37.411991,-122.079414'; - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); let summary = 'Here are the available routes for your request: \n\n'; routes.forEach((route, index) => { summary+=`Route ${index+1}: ${route.summary}. \n`; @@ -114,7 +114,7 @@ describe('Test cases for Google maps', () => { const source = await mapService.lookupGps('Denver'); const destination = await mapService.lookupGps('Yelllowstone national park'); - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); let polygon_path = ''; routes.forEach((route, index) => { polygon_path+=`&path=color:${mapService.get_random_color()}|weight:${5-index}|enc:${route.overview_polyline.points}`; @@ -134,7 +134,7 @@ describe('Test cases for Google maps', () => { ] // gerenate routes - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); let selected_route = routes[0]; let polygon_path = `&path=color:${mapService.get_random_color()}|enc:${selected_route.overview_polyline.points}`;; @@ -157,7 +157,7 @@ describe('Test cases for Google maps', () => { const source ='37.422391,-122.084845'; const destination = '37.411991,-122.079414'; - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); const polygon = routes[0].overview_polyline.points; await db.update_session('123', { polygon: polygon diff --git a/tests/unit/services/maps.test.js b/tests/unit/services/maps.test.js index 8a942f2..f4a7cd9 100644 --- a/tests/unit/services/maps.test.js +++ b/tests/unit/services/maps.test.js @@ -9,7 +9,16 @@ describe('Should test the map service', () => { const source ='37.422391,-122.084845'; const destination = '37.411991,-122.079414'; - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); + + expect(routes).to.be.an('array'); + }); + + it('Should test route fetching with strings', async () => { + const source ='Bengaluru'; + const destination = 'Mumbai'; + + let routes = await mapService.getRoutes({source: source, destination: destination}); expect(routes).to.be.an('array'); }); @@ -28,7 +37,7 @@ describe('Should test the map service', () => { const destination = '37.411991,-122.079414'; const point = [37.422391, -122.084845]; - let routes = await mapService.getRoutes(source, destination); + let routes = await mapService.getRoutes({source: source, destination: destination}); const status = await mapService.checkGpsOnPolygon(point, routes[0].overview_polyline.points); expect(status).to.be.true; @@ -38,7 +47,7 @@ describe('Should test the map service', () => { const source ='39.7392358,-104.990251'; const destination = '44.427963, -110.588455'; const pointBeforeCasper = [42.839531, -106.136404]; - await mapService.getRoutes(source, destination, pointBeforeCasper); + await mapService.getRoutes({source: source, destination: destination, avoidPoint: [pointBeforeCasper]}); }) }); From 97f52b694395d80011131e874bc0ea58fdc0a538 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Sun, 14 Apr 2024 21:09:22 +0530 Subject: [PATCH 02/11] updated sessions for the new method and tested workign search and select actions --- config/GPT/tools.js | 2 +- controllers/Agent.js | 114 ++++++++--------------- schemas/jsons/select.js | 41 +++++++++ server.js | 3 +- services/AI.js | 132 ++++++++++++++++++++++++--- services/Actions.js | 6 +- services/Conversation.js | 15 +++ tests/unit/controllers/agent.test.js | 69 +++++++++----- utils/logger.js | 7 ++ 9 files changed, 271 insertions(+), 118 deletions(-) create mode 100644 schemas/jsons/select.js create mode 100644 services/Conversation.js diff --git a/config/GPT/tools.js b/config/GPT/tools.js index 6986ced..dfc34d5 100644 --- a/config/GPT/tools.js +++ b/config/GPT/tools.js @@ -23,7 +23,7 @@ export const TOOLS = [ type: "function", function: { name: "perform_beckn_action", - description: "If the user is trying to search a product, select an item or add to cart, initialize an order or confirm on order.", + description: "If the user has indicated to search/find a product, select an item or add to cart, initialize an order or confirm on order.", parameters: { type: "object", properties: { diff --git a/controllers/Agent.js b/controllers/Agent.js index cca6ed2..91ab82b 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -1,91 +1,53 @@ -import MapsService from "../services/MapService.js"; -import { TOOLS } from "../config/GPT/tools.js"; -import logger from "../utils/logger.js"; -import OpenAI from "openai"; -import AI from "../services/AI.js"; -const map = new MapsService(); -const ai = new AI(); - -const openai = new OpenAI({ - apiKey: process.env.OPENAI_AI_KEY, -}) - -const AVAILABLE_TOOLS = { - get_routes: map.getRoutes.bind(map), - perform_beckn_action: ai.performAction.bind(ai), -}; +import AI from '../services/AI.js'; +import DBService from '../services/DBService.js' +import MapService from '../services/MapService.js' +import { + EMPTY_SESSION +} from '../config/constants.js'; +const db = new DBService(); +const map = new MapService(); async function getResponse(req, res) { const { From, Body } = req.body - // get session - - // get answer from AI - - // save session - - res.send(response) -} - - - - - -async function getResponseFromOpenAI(messages){ - const context = [ - {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, - {role: 'assistant', content : "You can only share results immediately, so you should never say that you will do something in the future. "}, - {role: 'assistant', content : "If the last tool call did not produce any useful response, you should convey that directly."}, - {role: 'assistant', content : "Your tone should be polite and helpful. "}, + if(!From || !Body){ + res.status(400).send("Bad Request") + } + else{ + // get session + const session_response = await db.get_session(From); + let session = session_response.data; + if(!session_response.status){ + session = EMPTY_SESSION + } - ] - try{ - const gpt_response = await openai.chat.completions.create({ - model: process.env.OPENAI_MODEL_ID, - messages: [...context, ...messages], - tools: TOOLS, - tool_choice: "auto", - }); - let responseMessage = gpt_response.choices[0].message; + // get answer from AI + const ai = new AI(); + ai.session = session; - // check for tool calls - const toolCalls = responseMessage.tool_calls; - if (toolCalls) { - logger.info("Tool calls found in response, proceeding..."); + // setup tools + const available_tools = { + get_routes: map.getRoutes.bind(map), + perform_beckn_action: ai.perform_beckn_transaction.bind(ai), + }; + ai.tools = available_tools; + // make request + let messages = [ + ...session.text, + { role: 'user', content: Body} + ]; + const response = await ai.getResponseFromOpenAI(messages) - messages.push(responseMessage); - - for (let tool of toolCalls) { - const parameters = JSON.parse(tool.function.arguments); - const functionToCall = AVAILABLE_TOOLS[tool.function.name]; - if (functionToCall) { - const response = await functionToCall(parameters); - - messages.push({ - tool_call_id: tool.id, - role: "tool", - name: functionToCall, - content: JSON.stringify(response), - }); + // save session + await db.update_session(From, session) - // call again to get the response - responseMessage = await getResponseFromOpenAI(messages); - } - } - } - - return responseMessage; - + res.send(response.content) } - catch(e){ - logger.error(e); - return false; - } + } export default { - getResponse, - getResponseFromOpenAI + getResponse } \ No newline at end of file diff --git a/schemas/jsons/select.js b/schemas/jsons/select.js new file mode 100644 index 0000000..cad0884 --- /dev/null +++ b/schemas/jsons/select.js @@ -0,0 +1,41 @@ +export default { + type: "object", + properties: { + order: { + type: "object", + description: "Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller.", + properties: { + items:{ + type: "array", + description: "The items purchased / availed in this order", + items: { + type: "object", + description: "'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.'", + properties: { + id: { + type: "string", + description: "ID of the item. In case of a select, this should be the id of item selected." + }, + quantity: { + type: "object", + description: "The selling quantity of the item. In case of a select, this should be the quantity selected by the user.", + properties: { + selected: { + type: "object", + description: "This represents the quantity selected for purchase of the item.", + properties: { + count: { + type: "number", + description: "The quantity selected for purchase." + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/server.js b/server.js index bb6c28c..acc8ffa 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import bodyParser from 'body-parser' import logger from './utils/logger.js' import messageController from './controllers/Bot.js' import DBService from './services/DBService.js' +import agentController from './controllers/Agent.js'; import { cancelBooking, updateCatalog, @@ -27,7 +28,7 @@ app.use(bodyParser.json()) // Define endpoints here // app.post('/act', actions.act) -app.post('/webhook', messageController.process_text) +app.post('/webhook', agentController.getResponse) app.post('/notify', notify) app.post('/cancel-booking', cancelBooking) app.post('/update-catalog', updateCatalog) diff --git a/services/AI.js b/services/AI.js index 3fb06e5..0bd7925 100644 --- a/services/AI.js +++ b/services/AI.js @@ -4,11 +4,14 @@ import logger from '../utils/logger.js' import yaml from 'js-yaml' import { v4 as uuidv4 } from 'uuid' import search from '../schemas/jsons/search.js'; +import select from '../schemas/jsons/select.js'; import actions from '../schemas/jsons/actions.js'; import Actions from './Actions.js'; +import { TOOLS } from '../config/GPT/tools.js'; const SCHEMAS = { - search: search + search: search, + select: select } const openai = new OpenAI({ @@ -24,6 +27,8 @@ class AI { this.action = null; this.bookings = []; this.actionService = new Actions(); + this.session = {}; + this.tools = []; } async get_beckn_action_from_text(instruction, context=[], last_action=null){ @@ -564,8 +569,8 @@ class AI { } } - async performAction({action, instruction}){ - logger.info(`performAction() : ${action}, ${instruction}`); + async perform_beckn_transaction({action, instruction}){ + logger.info(`perform_beckn_transaction() : ${action}, ${instruction}`); let response = { status: false, data: null, @@ -574,18 +579,33 @@ class AI { try{ // get context + logger.warn(`Getting context for action : ${action}`); const context = await this.get_context_by_action(action, instruction); + logger.info("Got context!"); // get message - const message = await this.get_message_by_action(action, instruction); + logger.warn(`Getting message for action : ${action}`); + const message = await this.get_message_by_action(action, instruction, context.domain); + logger.info("Got message!"); // call API + logger.warn(`Calling API for action : ${action}`); const url = `${context.base_url}/${action}`; const api_response = await this.actionService.call_api(url, 'POST', {context: context, message: message}); + logger.info("Got API response!"); response={ status: true, - data: api_response.data + data: api_response.data, + message: api_response.data.responses.length>0 ? "Succesfully retireved response" : "No response found for the given action" } + + + // update last action and response + if(api_response?.data?.responses?.length>0){ + this.session.profile.last_action = action; + this.session.beckn_transaction.responses[`on_${action}`] = api_response.data.responses; + } + } catch(e){ logger.error(e); @@ -604,7 +624,14 @@ class AI { let last_action_context=[]; if(action!='search'){ desired_structure.bpp_id = ``; - desired_structure.bpp_uri = ``; + desired_structure.bpp_uri = ``; + + // last action context + if(this.session?.profile?.last_action && this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`]){ + last_action_context = [ + {role: 'system', content: `Response of last action '${this.session.profile.last_action}' is : ${JSON.stringify(this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`])}`}, + ] + } } let response = { @@ -614,15 +641,14 @@ class AI { bap_id: registry_config[0].bap_subscriber_id, bap_uri: registry_config[0].bap_subscriber_url, action: action, - version: registry_config[0].version, - + version: registry_config[0].version, } - + const openai_messages = [ { role: 'system', content: `Your job is to analyse the given instruction, registry details and generate a config json in the following structure : ${JSON.stringify(desired_structure)}` }, { role: 'system', content: `Registry : ${JSON.stringify(registry_config)}` }, - { role: 'system', content: `Instruction : ${instruction}` }, ...last_action_context, + { role: 'system', content: `Instruction : ${instruction}` } ] try { @@ -634,7 +660,7 @@ class AI { }) let gpt_response = JSON.parse(completion.choices[0].message.content) response = {...response, ...gpt_response}; - logger.info(`Got context from instruction : ${JSON.stringify(response)}`); + logger.verbose(`Got context from instruction : ${JSON.stringify(response)}`); return response; } catch (e) { logger.error(e) @@ -642,12 +668,33 @@ class AI { } } - async get_message_by_action(action, instruction) { + async get_message_by_action(action, instruction, domain=null) { logger.info(`get_message_by_action() : ${action}, ${instruction}`) const messages = [ + { role: "assistant", content: `Current date is ${new Date().toISOString()}` }, { role: "user", content: instruction } ]; + + // Add domain context + let domain_context = []; + if(domain && registry_config[0].policies.domains[domain]){ + domain_context = [ + { role: 'system', content: `Domain : ${domain}`}, + { role: 'system', content: `Use the following policy : ${JSON.stringify(registry_config[0].policies.domains[domain])}` } + ] + } + + // last action context + let last_action_context=[]; + if(action!='search'){ + // last action context + if(this.session?.profile?.last_action && this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`]){ + last_action_context = [ + {role: 'system', content: `Response of last action '${this.session.profile.last_action}' is : ${JSON.stringify(this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`])}`}, + ] + } + } const schema = SCHEMAS[action]; @@ -666,12 +713,15 @@ class AI { // Assuming you have a function to abstract the API call const response = await openai.chat.completions.create({ model: 'gpt-4-0125-preview', - messages: messages, + messages: [ + ...domain_context, + ...last_action_context, + ...messages], tools: tools, tool_choice: "auto", // auto is default, but we'll be explicit }); const responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; - logger.info(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); + logger.verbose(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); return responseMessage } @@ -681,6 +731,60 @@ class AI { } } + async getResponseFromOpenAI(messages){ + + const context = [ + {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, + {role: 'assistant', content : "You can only share results immediately, so you should never say that you will do something in the future. "}, + {role: 'assistant', content : "If the last tool call did not produce any useful response, you should convey that directly."}, + {role: 'assistant', content : "Your tone should be polite and helpful. "}, + + ] + try{ + const gpt_response = await openai.chat.completions.create({ + model: process.env.OPENAI_MODEL_ID, + messages: [...context, ...messages], + tools: TOOLS, + tool_choice: "auto", + }); + let responseMessage = gpt_response.choices[0].message; + + // check for tool calls + const toolCalls = responseMessage.tool_calls; + if (toolCalls) { + logger.info("Tool calls found in response, proceeding..."); + + + messages.push(responseMessage); + + for (let tool of toolCalls) { + const parameters = JSON.parse(tool.function.arguments); + const functionToCall = this.tools[tool.function.name]; + if (functionToCall) { + const response = await functionToCall(parameters); + + messages.push({ + tool_call_id: tool.id, + role: "tool", + name: functionToCall, + content: JSON.stringify(response), + }); + + // call again to get the response + responseMessage = await this.getResponseFromOpenAI(messages); + } + } + } + + return responseMessage; + + } + catch(e){ + logger.error(e); + return false; + } + } + } diff --git a/services/Actions.js b/services/Actions.js index a1d5813..fef5b89 100644 --- a/services/Actions.js +++ b/services/Actions.js @@ -22,8 +22,8 @@ class Actions { } async call_api(endpoint, method, data, headers = {}) { - logger.info(`Calling ${method} on ${endpoint}...`); - logger.info(`Data: ${JSON.stringify(data, null, 2)}`); + logger.warn(`Calling ${method} on ${endpoint}...`); + logger.verbose(`Data: ${JSON.stringify(data, null, 2)}`); let responseObject = { status: false, retry: false, @@ -54,7 +54,7 @@ class Actions { cookies: response.headers['set-cookie'], } logger.info(`API call was successful: , response.status`) - logger.info(JSON.stringify(response.data, null, 2)) + logger.verbose(JSON.stringify(response.data, null, 2)) } catch (error) { logger.error(error) diff --git a/services/Conversation.js b/services/Conversation.js new file mode 100644 index 0000000..cafe3bb --- /dev/null +++ b/services/Conversation.js @@ -0,0 +1,15 @@ +class Conversation{ + constructor(session) { + this.session = session; + } + + async getResponse(message) { + let response = {}; + + // get answer from AI + + return response; + } +} + +export default Conversation; \ No newline at end of file diff --git a/tests/unit/controllers/agent.test.js b/tests/unit/controllers/agent.test.js index 2026d4e..d760ba7 100644 --- a/tests/unit/controllers/agent.test.js +++ b/tests/unit/controllers/agent.test.js @@ -1,33 +1,56 @@ import { describe, it} from 'mocha' import * as chai from 'chai' -import agent from '../../../controllers/Agent.js'; const expect = chai.expect +import request from 'supertest' +import app from '../../../server.js' -describe.only('API tests for getResponseFromOpenAI() function', () => { - it('Should return a string message for a common question', async ()=>{ - let messages = [ - { role: 'user', content: "What is the capital of India?" }, - ]; - - const response = await agent.getResponseFromOpenAI(messages); - expect(response.content).to.be.a('string'); - expect(response.content).to.contain('New Delhi'); +describe.only('API tests for getResponse() function', () => { + it('should return 400 if From or Body is missing', async () => { + const message = "What is the capital of India?" + const response = await request(app).post('/webhook').send({ + Body: message, + }) + expect(response.status).to.be.eq(400) + expect(response.text).to.be.eq('Bad Request'); }) - it('Should return routes when asked for routes', async ()=>{ - let messages = [ - { role: 'user', content: "Can you get me routes from Delhi to Mumbai?"}, - ]; - const response = await agent.getResponseFromOpenAI(messages); - expect(response.content).to.be.a('string'); - expect(response.content).to.contain('NH 48'); + it('should return sucecsful response if a general query is asked', async () => { + const message = "What is the capital of India?" + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: message, + }) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('New Delhi'); }) - it('Should be able to search for items on network', async ()=>{ - let messages = [ - { role: 'user', content: "Can you find some ev chargers near Denver?"}, - ]; - const response = await agent.getResponseFromOpenAI(messages); - expect(response.content).to.be.a('string'); + it('Should return list of routes between two points if asked', async () => { + const message = "Can you share routes between New Delhi and Mumbai?" + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: message, + }) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('NH 48'); + }) + + it.only('Should return a list of hotels', async () => { + const message = "Can you please find hotels near Yellowstone national park?" + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: message, + }) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('Lake'); + }) + + it.only('Should return a list of hotels', async () => { + const message = "Lets select the first one." + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: message, + }) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('Lake'); }) }) \ No newline at end of file diff --git a/utils/logger.js b/utils/logger.js index 6e6c9f2..14ec52e 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -1,7 +1,14 @@ import { createLogger, format, transports } from 'winston' +const colorizer = format.colorize(); const logger = createLogger({ + level: 'silly', format: format.combine( + format((info) => { + info.level = colorizer.colorize(info.level, `${info.level} ${info.message}`); + info.message = ''; + return info; + })(), format.colorize({ level: true, // Colorize the level of each log message colors: { From c3643a73b6e08678cf4db1365cff7245f5f09e59 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Mon, 15 Apr 2024 17:25:09 +0530 Subject: [PATCH 03/11] Added schemas for init and confirm, added retries and fixes --- config/constants.js | 4 + config/registry.json | 2 +- controllers/Agent.js | 2 + schemas/jsons/confirm.js | 109 +++++++++++++++++++++++++++ schemas/jsons/init.js | 61 +++++++++++++++ services/AI.js | 101 ++++++++++++++++++------- tests/unit/controllers/agent.test.js | 32 +++++--- 7 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 schemas/jsons/confirm.js create mode 100644 schemas/jsons/init.js diff --git a/config/constants.js b/config/constants.js index b927e2b..62d910d 100644 --- a/config/constants.js +++ b/config/constants.js @@ -47,6 +47,10 @@ export const BECKN_STATUS_CALL = { export const EMPTY_BECKN_TRANSACTION = { id: false, responses: { + search: {}, + select: {}, + init: {}, + confirm: {}, on_search: {}, on_select: {}, on_init: {}, diff --git a/config/registry.json b/config/registry.json index 16bd39f..3746bac 100644 --- a/config/registry.json +++ b/config/registry.json @@ -24,7 +24,7 @@ }, "hospitality": { "rules": [ - "item.descriptor should not be used in search intent for this domain", + "item must not be used in search intent for this domain", "search must have two stops for this domain.", "Supported stop.type : check-in, check-out", "fulfillment.stops[i].time should be an object and contain timestamp", diff --git a/controllers/Agent.js b/controllers/Agent.js index 91ab82b..448bfcf 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -38,6 +38,8 @@ async function getResponse(req, res) { { role: 'user', content: Body} ]; const response = await ai.getResponseFromOpenAI(messages) + messages.push(response); + session.text = messages; // Update session text (chat history) // save session await db.update_session(From, session) diff --git a/schemas/jsons/confirm.js b/schemas/jsons/confirm.js new file mode 100644 index 0000000..d834972 --- /dev/null +++ b/schemas/jsons/confirm.js @@ -0,0 +1,109 @@ +export default { + type: "object", + properties: { + order: { + type: "object", + description: "Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller.", + properties: { + items:{ + type: "array", + description: "The items purchased / availed in this order as per the 'init' request payload.", + items: { + type: "object", + description: "'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.'", + properties: { + id: { + type: "string", + description: "ID of the item. In case of a select, this should be the id of item selected." + }, + quantity: { + type: "object", + description: "The selling quantity of the item. In case of a select, this should be the quantity selected by the user.", + properties: { + selected: { + type: "object", + description: "This represents the quantity selected for purchase of the item.", + properties: { + count: { + type: "number", + description: "The quantity selected for purchase." + } + } + } + } + } + } + } + }, + billing: { + description: "Billing details as per the 'init' request payload.", + type: "object", + properties: { + name: { + type: "string", + description: "Name of the billable entity" + }, + email: { + type: "string", + description: "Email address where the bill is sent to" + }, + phone: { + type: "string", + description: "Phone number of the billable entity" + } + }, + required: ["name", "email", "phone"] + }, + fulfillments: { + description: "The fulfillments involved in completing this order.", + type: "array", + items: { + type: "object", + description: "Describes how a an order will be rendered/fulfilled to the end-customer.", + properties: { + id: { + type: "string", + description: "Unique reference ID to the fulfillment of an order based on fulfillments provided in the init response." + }, + customer: { + type: "object", + description: "The person that will ultimately receive the order", + properties: { + person: { + type: "object", + description: " Describes a person as any individual ", + properties: { + name: { + type: "string", + description: "Name of the person" + } + }, + required: ["name"] + }, + contact: { + type: "object", + description: "Describes the contact information of an entity", + properties: { + phone: { + type: "string", + description: "Phone number of the contact person" + }, + email: { + type: "string", + description: "Email address of the contact person" + } + }, + required: ["phone", "email"] + } + }, + required: ["person", "contact"] + } + }, + required: ["customer"] + } + } + }, + required: ["items", "billing", "fulfillments"] + } + } +} \ No newline at end of file diff --git a/schemas/jsons/init.js b/schemas/jsons/init.js new file mode 100644 index 0000000..89b2eb8 --- /dev/null +++ b/schemas/jsons/init.js @@ -0,0 +1,61 @@ +export default { + type: "object", + properties: { + order: { + type: "object", + description: "Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller.", + properties: { + items:{ + type: "array", + description: "The items purchased / availed in this order as per the 'select' request payload.", + items: { + type: "object", + description: "'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.'", + properties: { + id: { + type: "string", + description: "ID of the item. In case of a select, this should be the id of item selected." + }, + quantity: { + type: "object", + description: "The selling quantity of the item. In case of a select, this should be the quantity selected by the user.", + properties: { + selected: { + type: "object", + description: "This represents the quantity selected for purchase of the item.", + properties: { + count: { + type: "number", + description: "The quantity selected for purchase." + } + } + } + } + } + } + } + }, + billing: { + description: "Describes the billing details of an entity.
This has properties like name,organization,address,email,phone,time,tax_number, created_at,updated_at", + type: "object", + properties: { + name: { + type: "string", + description: "Name of the billable entity" + }, + email: { + type: "string", + description: "Email address where the bill is sent to" + }, + phone: { + type: "string", + description: "Phone number of the billable entity" + } + }, + required: ["name", "email", "phone"] + } + }, + required: ["items", "billing"] + } + } +} \ No newline at end of file diff --git a/services/AI.js b/services/AI.js index 0bd7925..2491a86 100644 --- a/services/AI.js +++ b/services/AI.js @@ -3,16 +3,31 @@ import OpenAI from 'openai' import logger from '../utils/logger.js' import yaml from 'js-yaml' import { v4 as uuidv4 } from 'uuid' -import search from '../schemas/jsons/search.js'; -import select from '../schemas/jsons/select.js'; import actions from '../schemas/jsons/actions.js'; import Actions from './Actions.js'; import { TOOLS } from '../config/GPT/tools.js'; -const SCHEMAS = { - search: search, - select: select +// TODO: Load schemas. This needs to be improved so that any new schema is automatically loaded +import search from '../schemas/jsons/search.js'; +import select from '../schemas/jsons/select.js'; +import init from '../schemas/jsons/init.js'; +import confirm from '../schemas/jsons/confirm.js'; +import get_text_by_key from '../utils/language.js'; +const BECKN_ACTIONS = { + search: { + schema : search, call_to_action : "Which one would you like to select?" + }, + select: { + schema: select, call_to_action: "Would you like to initiate the order?" + }, + init: { + schema: init, call_to_action: "Would you like to confirm the order?" + }, + confirm: { + schema: confirm, call_to_action: "Your order is confirmed with order id . Would you like to order something else?" + } } +const NUMBER_OF_RETRIES=3; const openai = new OpenAI({ apiKey: process.env.OPENAI_AI_KEY, @@ -29,6 +44,7 @@ class AI { this.actionService = new Actions(); this.session = {}; this.tools = []; + this.attempt = 0; // for API call attempts } async get_beckn_action_from_text(instruction, context=[], last_action=null){ @@ -578,34 +594,62 @@ class AI { } try{ + + let context = {}; + let message = {}; + let api_response = {}; + // get context - logger.warn(`Getting context for action : ${action}`); - const context = await this.get_context_by_action(action, instruction); - logger.info("Got context!"); + let attempt = 0; + while(attempt0 ? "Succesfully retireved response" : "No response found for the given action" - } - - // update last action and response - if(api_response?.data?.responses?.length>0){ - this.session.profile.last_action = action; - this.session.beckn_transaction.responses[`on_${action}`] = api_response.data.responses; + if(api_response?.status && api_response?.data?.responses?.length>0){ + response={ + status: true, + data: api_response.data, + message: api_response.data.responses.length>0 ? BECKN_ACTIONS[action]['call_to_action'] : "No response found for the given action" + } + + + // update last action and response + if(api_response?.data?.responses?.length>0){ + this.session.profile.last_action = action; + this.session.beckn_transaction.responses[action] = request; + this.session.beckn_transaction.responses[`on_${action}`] = api_response.data.responses; + } + } + else if(this.attempt<=NUMBER_OF_RETRIES){ + // retry + this.attempt++; + logger.warn(`Retrying perform_beckn_transaction() for action : ${action} | Attempt : ${this.attempt+1}...`); + response = await this.perform_beckn_transaction({action, instruction}); + } + else{ + throw new Error(get_text_by_key('api_call_failed')); } - } catch(e){ logger.error(e); @@ -689,14 +733,15 @@ class AI { let last_action_context=[]; if(action!='search'){ // last action context - if(this.session?.profile?.last_action && this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`]){ + let prefix = this.session?.profile?.last_action=='search' ? 'on_' : ''; + if(this.session?.profile?.last_action && this.session.beckn_transaction?.responses[`${prefix}${this.session.profile.last_action}`]){ last_action_context = [ - {role: 'system', content: `Response of last action '${this.session.profile.last_action}' is : ${JSON.stringify(this.session.beckn_transaction?.responses[`on_${this.session.profile.last_action}`])}`}, + {role: 'system', content: `Payload of '${prefix}${this.session.profile.last_action}' is : ${JSON.stringify(this.session.beckn_transaction?.responses[`${prefix}${this.session.profile.last_action}`])}`}, ] } } - const schema = SCHEMAS[action]; + const schema = BECKN_ACTIONS[action]['schema']; const tools = [ { @@ -712,7 +757,7 @@ class AI { try{ // Assuming you have a function to abstract the API call const response = await openai.chat.completions.create({ - model: 'gpt-4-0125-preview', + model: 'gpt-4-0125-preview', //process.env.OPENAI_MODEL_ID, messages: [ ...domain_context, ...last_action_context, diff --git a/tests/unit/controllers/agent.test.js b/tests/unit/controllers/agent.test.js index d760ba7..71cf4d2 100644 --- a/tests/unit/controllers/agent.test.js +++ b/tests/unit/controllers/agent.test.js @@ -3,8 +3,9 @@ import * as chai from 'chai' const expect = chai.expect import request from 'supertest' import app from '../../../server.js' +import logger from '../../../utils/logger.js' -describe.only('API tests for getResponse() function', () => { +describe('API tests for getResponse() function', () => { it('should return 400 if From or Body is missing', async () => { const message = "What is the capital of India?" const response = await request(app).post('/webhook').send({ @@ -34,7 +35,7 @@ describe.only('API tests for getResponse() function', () => { expect(response.text).to.contain('NH 48'); }) - it.only('Should return a list of hotels', async () => { + it('Should return a list of hotels', async () => { const message = "Can you please find hotels near Yellowstone national park?" const response = await request(app).post('/webhook').send({ From: process.env.TEST_RECEPIENT_NUMBER, @@ -43,14 +44,25 @@ describe.only('API tests for getResponse() function', () => { expect(response.text).to.be.a('string'); expect(response.text).to.contain('Lake'); }) +}) - it.only('Should return a list of hotels', async () => { - const message = "Lets select the first one." - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: message, +describe.only('API tests for a order confirmation workflow', ()=>{ + const chats = [ + {key: "search_hotel", value: "Can you please find hotels near Yellowstone national park?"}, + {key: "select_hotel", value: "Lets select the first one."}, + {key: "initiate_order", value: "Lets initiate the order. My details are : John Doe, 1234567890"}, + {key: "confirm_order", value: "Lets confirm."} + ] + + for(const chat of chats){ + it(`Should return a response for ${chat.key}`, async () => { + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: chat.value, + }) + logger.info(`Response for ${chat.key} : ${response.text}`) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('Lake'); }) - expect(response.text).to.be.a('string'); - expect(response.text).to.contain('Lake'); - }) + } }) \ No newline at end of file From d20ea81b619e7ef305dfb675df0e701b4472ce4c Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Tue, 16 Apr 2024 11:25:59 +0530 Subject: [PATCH 04/11] fixed test cases --- services/AI.js | 6 +++--- tests/unit/controllers/agent.test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/AI.js b/services/AI.js index 2491a86..d7a4c55 100644 --- a/services/AI.js +++ b/services/AI.js @@ -629,7 +629,7 @@ class AI { if(api_response?.status && api_response?.data?.responses?.length>0){ response={ status: true, - data: api_response.data, + data: api_response?.data?.responses, message: api_response.data.responses.length>0 ? BECKN_ACTIONS[action]['call_to_action'] : "No response found for the given action" } @@ -641,8 +641,8 @@ class AI { this.session.beckn_transaction.responses[`on_${action}`] = api_response.data.responses; } } - else if(this.attempt<=NUMBER_OF_RETRIES){ - // retry + else if(this.attempt{ const chats = [ {key: "search_hotel", value: "Can you please find hotels near Yellowstone national park?"}, {key: "select_hotel", value: "Lets select the first one."}, - {key: "initiate_order", value: "Lets initiate the order. My details are : John Doe, 1234567890"}, + {key: "initiate_order", value: "Lets initiate the order. My details are : John Doe, 1234567890, john.doe@example.com"}, {key: "confirm_order", value: "Lets confirm."} ] From dd23470ee1f3372cb95a4989b19167c6e76c5821 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Tue, 16 Apr 2024 12:23:26 +0530 Subject: [PATCH 05/11] Tested ene to en primary booking flow --- config/GPT/tools.js | 16 +++++++++ controllers/Agent.js | 7 ++-- schemas/jsons/actions.js | 2 +- schemas/jsons/confirm.js | 49 ---------------------------- schemas/jsons/context.js | 2 +- schemas/jsons/init.js | 30 ----------------- services/AI.js | 9 +++++ services/MapService.js | 16 +++++++++ tests/unit/controllers/agent.test.js | 14 ++++++-- 9 files changed, 59 insertions(+), 86 deletions(-) diff --git a/config/GPT/tools.js b/config/GPT/tools.js index dfc34d5..d734a2f 100644 --- a/config/GPT/tools.js +++ b/config/GPT/tools.js @@ -19,6 +19,22 @@ export const TOOLS = [ } } }, + { + type: "function", + function: { + name: "select_route", + description: "This function should only be used if the user has selected one of the routes shared by the assistant in the previous step. This function must not be used if the last response from assistant was not a list of routes between two points.", + parameters: { + type: "object", + properties: { + index:{ + type:"number", + description: "Index of the selected route." + } + } + } + } + }, { type: "function", function: { diff --git a/controllers/Agent.js b/controllers/Agent.js index 448bfcf..d79955d 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -5,7 +5,6 @@ import { EMPTY_SESSION } from '../config/constants.js'; const db = new DBService(); -const map = new MapService(); async function getResponse(req, res) { const { From, Body } = req.body @@ -21,13 +20,15 @@ async function getResponse(req, res) { session = EMPTY_SESSION } - // get answer from AI + // initialize services const ai = new AI(); - ai.session = session; + const map = new MapService(); + ai.session = map.session = session; // setup tools const available_tools = { get_routes: map.getRoutes.bind(map), + select_route: map.selectRoute.bind(map), perform_beckn_action: ai.perform_beckn_transaction.bind(ai), }; ai.tools = available_tools; diff --git a/schemas/jsons/actions.js b/schemas/jsons/actions.js index a319983..7eb6c53 100644 --- a/schemas/jsons/actions.js +++ b/schemas/jsons/actions.js @@ -3,7 +3,7 @@ export default { properties: { action:{ type:"string", - description: "action that the user wants to perform. This should be one of th actions defined by supported actions. If its not one of teh actions, its value should be null." + description: "action that the user wants to perform. This should be one of th actions defined by supported actions. If its not one of the actions, its value should be null." }, transaction_id:{ type:"string", diff --git a/schemas/jsons/confirm.js b/schemas/jsons/confirm.js index d834972..11bf231 100644 --- a/schemas/jsons/confirm.js +++ b/schemas/jsons/confirm.js @@ -5,55 +5,6 @@ export default { type: "object", description: "Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller.", properties: { - items:{ - type: "array", - description: "The items purchased / availed in this order as per the 'init' request payload.", - items: { - type: "object", - description: "'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.'", - properties: { - id: { - type: "string", - description: "ID of the item. In case of a select, this should be the id of item selected." - }, - quantity: { - type: "object", - description: "The selling quantity of the item. In case of a select, this should be the quantity selected by the user.", - properties: { - selected: { - type: "object", - description: "This represents the quantity selected for purchase of the item.", - properties: { - count: { - type: "number", - description: "The quantity selected for purchase." - } - } - } - } - } - } - } - }, - billing: { - description: "Billing details as per the 'init' request payload.", - type: "object", - properties: { - name: { - type: "string", - description: "Name of the billable entity" - }, - email: { - type: "string", - description: "Email address where the bill is sent to" - }, - phone: { - type: "string", - description: "Phone number of the billable entity" - } - }, - required: ["name", "email", "phone"] - }, fulfillments: { description: "The fulfillments involved in completing this order.", type: "array", diff --git a/schemas/jsons/context.js b/schemas/jsons/context.js index a319983..7eb6c53 100644 --- a/schemas/jsons/context.js +++ b/schemas/jsons/context.js @@ -3,7 +3,7 @@ export default { properties: { action:{ type:"string", - description: "action that the user wants to perform. This should be one of th actions defined by supported actions. If its not one of teh actions, its value should be null." + description: "action that the user wants to perform. This should be one of th actions defined by supported actions. If its not one of the actions, its value should be null." }, transaction_id:{ type:"string", diff --git a/schemas/jsons/init.js b/schemas/jsons/init.js index 89b2eb8..23a8a9d 100644 --- a/schemas/jsons/init.js +++ b/schemas/jsons/init.js @@ -5,36 +5,6 @@ export default { type: "object", description: "Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller.", properties: { - items:{ - type: "array", - description: "The items purchased / availed in this order as per the 'select' request payload.", - items: { - type: "object", - description: "'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.'", - properties: { - id: { - type: "string", - description: "ID of the item. In case of a select, this should be the id of item selected." - }, - quantity: { - type: "object", - description: "The selling quantity of the item. In case of a select, this should be the quantity selected by the user.", - properties: { - selected: { - type: "object", - description: "This represents the quantity selected for purchase of the item.", - properties: { - count: { - type: "number", - description: "The quantity selected for purchase." - } - } - } - } - } - } - } - }, billing: { description: "Describes the billing details of an entity.
This has properties like name,organization,address,email,phone,time,tax_number, created_at,updated_at", type: "object", diff --git a/services/AI.js b/services/AI.js index d7a4c55..43470a3 100644 --- a/services/AI.js +++ b/services/AI.js @@ -768,6 +768,15 @@ class AI { const responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; logger.verbose(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); + // update message for init and confirm. Cleanup incorrect `items` for init and incorrect `items` and `billing` details for confirm + if((action=='init' || action=='confirm') && this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]){ + responseMessage.order = { + ...responseMessage.order, + ...this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]?.message?.order + } + + } + return responseMessage } catch(e){ diff --git a/services/MapService.js b/services/MapService.js index 085a19a..63335d0 100644 --- a/services/MapService.js +++ b/services/MapService.js @@ -9,6 +9,7 @@ import get_text_by_key from "../utils/language.js"; class MapsService { constructor() { this.client = new Client({}); + this.session = {}; } /** @@ -38,6 +39,11 @@ class MapsService { const path = this.get_static_image_path(routes); logger.info(`Static image path for routes: ${path}`); + // Save session if possible + if(this.session){ + this.session.routes = routes; + } + return routes.map(route => route.summary); } catch (error) { logger.error(error); @@ -64,6 +70,16 @@ class MapsService { } } + async selectRoute(index) { + logger.info(`Selecting route ${index}`); + if (this.session.routes && index >= 0 && index < this.session.routes.length) { + this.session.profile.selected_route = this.session.routes[index]; + return true; + } else { + return false; + } + } + get_random_color() { const red = Math.floor(Math.random() * 256); // Generate a random integer between 0 and 255 for red const green = Math.floor(Math.random() * 256); // Generate a random integer between 0 and 255 for green diff --git a/tests/unit/controllers/agent.test.js b/tests/unit/controllers/agent.test.js index b1a10eb..9bd380a 100644 --- a/tests/unit/controllers/agent.test.js +++ b/tests/unit/controllers/agent.test.js @@ -25,7 +25,7 @@ describe('API tests for getResponse() function', () => { expect(response.text).to.contain('New Delhi'); }) - it('Should return list of routes between two points if asked', async () => { + it.only('Should return list of routes between two points if asked', async () => { const message = "Can you share routes between New Delhi and Mumbai?" const response = await request(app).post('/webhook').send({ From: process.env.TEST_RECEPIENT_NUMBER, @@ -35,6 +35,16 @@ describe('API tests for getResponse() function', () => { expect(response.text).to.contain('NH 48'); }) + it.only('Should select a route', async () => { + const message = "Lets select the first one" + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: message, + }) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('first'); + }) + it('Should return a list of hotels', async () => { const message = "Can you please find hotels near Yellowstone national park?" const response = await request(app).post('/webhook').send({ @@ -46,7 +56,7 @@ describe('API tests for getResponse() function', () => { }) }) -describe.only('API tests for a order confirmation workflow', ()=>{ +describe('API tests for a order confirmation workflow', ()=>{ const chats = [ {key: "search_hotel", value: "Can you please find hotels near Yellowstone national park?"}, {key: "select_hotel", value: "Lets select the first one."}, From bdd132372f2097d0640210caa392f9be942a1ffb Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Tue, 16 Apr 2024 16:12:52 +0530 Subject: [PATCH 06/11] - Added searching along the route polyline --- config/registry.json | 2 +- schemas/jsons/search.js | 4 ++-- services/AI.js | 44 ++++++++++++++++++++++++++++++++--------- services/MapService.js | 8 +++++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/config/registry.json b/config/registry.json index 3746bac..0ed8423 100644 --- a/config/registry.json +++ b/config/registry.json @@ -18,7 +18,7 @@ "rules": [ "item.descriptor should not be used in search intent for this domain", "search should have fulfillment for this domain. fulfillment should only contain location for this domain.", - "fulfillment should contain only 1 stop for this domain", + "fulfillment must have only 1 stop for this domain", "If a route polygon has been shared, fulfillment.stops[i].location should have the polygon field." ] }, diff --git a/schemas/jsons/search.js b/schemas/jsons/search.js index aea2be3..b297dd9 100644 --- a/schemas/jsons/search.js +++ b/schemas/jsons/search.js @@ -72,10 +72,10 @@ export default { }, polygon: { type: "string", - description: "This describes the route on which the item needs to be searched. This should be used if the request is to search along a route" + description: "This describes the route on which the item needs to be searched. This should be used if the instruction is to search along a route and there is a selelected route in user profile." } }, - required: ['gps'] + required: [] }, time: { type: "object", diff --git a/services/AI.js b/services/AI.js index 43470a3..5dc7941 100644 --- a/services/AI.js +++ b/services/AI.js @@ -740,6 +740,14 @@ class AI { ] } } + + // Add profile context + let profile_context = []; + if(this.session?.profile){ + profile_context = [ + { role: 'system', content: `User profile : ${JSON.stringify(this.session.profile)}` } + ] + } const schema = BECKN_ACTIONS[action]['schema']; @@ -761,21 +769,17 @@ class AI { messages: [ ...domain_context, ...last_action_context, + ...profile_context, ...messages], tools: tools, tool_choice: "auto", // auto is default, but we'll be explicit }); - const responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; + let responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; + + responseMessage = await this._cleanup_beckn_message(action, responseMessage); + logger.verbose(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); - // update message for init and confirm. Cleanup incorrect `items` for init and incorrect `items` and `billing` details for confirm - if((action=='init' || action=='confirm') && this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]){ - responseMessage.order = { - ...responseMessage.order, - ...this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]?.message?.order - } - - } return responseMessage } @@ -785,6 +789,27 @@ class AI { } } + async _cleanup_beckn_message(action, message){ + // cleanup polygon + if(action=='search' && message.intent.fulfillment.stops){ + for(let stop of message.intent.fulfillment.stops){ + if(stop.location?.polygon){ + delete stop.location?.gps; + } + } + } + + // update message for init and confirm. Cleanup incorrect `items` for init and incorrect `items` and `billing` details for confirm + if((action=='init' || action=='confirm') && this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]){ + message.order = { + ...message.order, + ...this.session?.beckn_transaction?.responses[this.session?.profile?.last_action]?.message?.order + } + } + + return message; + } + async getResponseFromOpenAI(messages){ const context = [ @@ -812,6 +837,7 @@ class AI { messages.push(responseMessage); for (let tool of toolCalls) { + logger.warn(`Executing tool : ${tool.function.name} ...`); const parameters = JSON.parse(tool.function.arguments); const functionToCall = this.tools[tool.function.name]; if (functionToCall) { diff --git a/services/MapService.js b/services/MapService.js index 63335d0..4e9238a 100644 --- a/services/MapService.js +++ b/services/MapService.js @@ -71,9 +71,11 @@ class MapsService { } async selectRoute(index) { - logger.info(`Selecting route ${index}`); - if (this.session.routes && index >= 0 && index < this.session.routes.length) { - this.session.profile.selected_route = this.session.routes[index]; + logger.info(`Selecting route ${index.index}`); + if (this.session.routes && index.index >= 0 && index.index < this.session.routes.length) { + this.session.profile.selected_route = { + polyline: this.session.routes[index.index].overview_polyline.points + } return true; } else { return false; From c37e9bc0226e1afc1d9091936496ffa99b71134c Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Tue, 16 Apr 2024 16:42:31 +0530 Subject: [PATCH 07/11] Added raw response if raw_yn is sent --- controllers/Agent.js | 11 ++++++++--- services/AI.js | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/controllers/Agent.js b/controllers/Agent.js index d79955d..3cbd2d4 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -7,7 +7,7 @@ import { const db = new DBService(); async function getResponse(req, res) { - const { From, Body } = req.body + const { From, Body, raw_yn } = req.body if(!From || !Body){ res.status(400).send("Bad Request") @@ -38,14 +38,19 @@ async function getResponse(req, res) { ...session.text, { role: 'user', content: Body} ]; - const response = await ai.getResponseFromOpenAI(messages) + const response = await ai.getResponseFromOpenAI(messages, raw_yn) + + // prepare raw body if required + const responseBody = raw_yn ? response.raw : response.content; + delete response.raw; + messages.push(response); session.text = messages; // Update session text (chat history) // save session await db.update_session(From, session) - res.send(response.content) + res.send(responseBody) } } diff --git a/services/AI.js b/services/AI.js index 5dc7941..0f5bcad 100644 --- a/services/AI.js +++ b/services/AI.js @@ -810,7 +810,7 @@ class AI { return message; } - async getResponseFromOpenAI(messages){ + async getResponseFromOpenAI(messages, raw_yn=false){ const context = [ {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, @@ -852,6 +852,9 @@ class AI { // call again to get the response responseMessage = await this.getResponseFromOpenAI(messages); + if(raw_yn) { + responseMessage.raw = response.data; + } } } } From 73b62a4b295cb2318a2e190fd29e468503ddae27 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Tue, 16 Apr 2024 23:25:42 +0530 Subject: [PATCH 08/11] Added whatsapp message --- controllers/Agent.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/controllers/Agent.js b/controllers/Agent.js index 3cbd2d4..65701d9 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -1,13 +1,17 @@ import AI from '../services/AI.js'; import DBService from '../services/DBService.js' import MapService from '../services/MapService.js' +import Actions from '../services/Actions.js'; import { EMPTY_SESSION } from '../config/constants.js'; const db = new DBService(); +const actionsService = new Actions(); async function getResponse(req, res) { const { From, Body, raw_yn } = req.body + const format = (req?.headers && req.headers['content-type']) || 'text/xml'; + if(!From || !Body){ res.status(400).send("Bad Request") @@ -50,9 +54,15 @@ async function getResponse(req, res) { // save session await db.update_session(From, session) - res.send(responseBody) - } - + // Send response + if(format!='application/json'){ + await actionsService.send_message(From, responseBody, []); + res.send("Message sent!") + } + else{ + res.send(responseBody) + } + } } From 095a8ef0b0614beb2efc008889f2857b3eacd7b7 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Wed, 17 Apr 2024 00:41:38 +0530 Subject: [PATCH 09/11] Cleanup old structure code and removed unused files and directories --- .eslintignore | 2 +- config/GPT/becknGPT_instructions.txt | 1 - config/GPT/becknGPT_schema.yml | 64 - config/language.json | 67 +- config/openai.json | 17 - {schemas/jsons => config/schemas}/confirm.js | 0 {schemas/jsons => config/schemas}/context.js | 0 {schemas/jsons => config/schemas}/init.js | 0 {schemas/jsons => config/schemas}/search.js | 0 {schemas/jsons => config/schemas}/select.js | 0 controllers/Agent.js | 2 +- controllers/Bot.js | 363 ---- controllers/ControlCenter.js | 211 +- package.json | 4 +- schemas/core_1.1.0.yml | 1833 ------------------ schemas/core_1.1.0/confirm.yml | 236 --- schemas/core_1.1.0/init.yml | 153 -- schemas/core_1.1.0/search.yml | 122 -- schemas/core_1.1.0/select.yml | 132 -- schemas/jsons/actions.js | 13 - server.js | 5 +- services/AI.js | 645 +----- services/Actions.js | 1 - services/Conversation.js | 15 - services/MapService.js | 55 - tests/apis/agent.test.js | 252 --- tests/apis/apis.test.js | 34 + tests/unit/controllers/agent.test.js | 27 +- tests/unit/controllers/bot.test.js | 169 -- tests/unit/controllers/controlCenter.test.js | 2 +- tests/unit/services/actions.test.js | 8 +- tests/unit/services/ai.test.js | 352 +--- tests/unit/services/maps.test.js | 3 +- 33 files changed, 298 insertions(+), 4490 deletions(-) delete mode 100644 config/GPT/becknGPT_instructions.txt delete mode 100644 config/GPT/becknGPT_schema.yml delete mode 100644 config/openai.json rename {schemas/jsons => config/schemas}/confirm.js (100%) rename {schemas/jsons => config/schemas}/context.js (100%) rename {schemas/jsons => config/schemas}/init.js (100%) rename {schemas/jsons => config/schemas}/search.js (100%) rename {schemas/jsons => config/schemas}/select.js (100%) delete mode 100644 controllers/Bot.js delete mode 100644 schemas/core_1.1.0.yml delete mode 100644 schemas/core_1.1.0/confirm.yml delete mode 100644 schemas/core_1.1.0/init.yml delete mode 100644 schemas/core_1.1.0/search.yml delete mode 100644 schemas/core_1.1.0/select.yml delete mode 100644 schemas/jsons/actions.js delete mode 100644 services/Conversation.js delete mode 100644 tests/apis/agent.test.js create mode 100644 tests/apis/apis.test.js delete mode 100644 tests/unit/controllers/bot.test.js diff --git a/.eslintignore b/.eslintignore index 74dc836..f4ef34a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,4 @@ models/* package*.json LICENSE *.md -schemas/* \ No newline at end of file +config/schemas/* \ No newline at end of file diff --git a/config/GPT/becknGPT_instructions.txt b/config/GPT/becknGPT_instructions.txt deleted file mode 100644 index 71c6d1f..0000000 --- a/config/GPT/becknGPT_instructions.txt +++ /dev/null @@ -1 +0,0 @@ -BecknGPT is designed to assist users with shopping needs, efficiently managing tasks from product searches to order confirmations. When a user indicates a desire to init an order, BecknGPT will ask for essential details such as name, email, phone number, and addresses before making the API call. During the init call, always mention that 'fulfillment details same as billing details' in the context. This information is then carefully included under the context field in API calls, ensuring a personalized and secure shopping experience. After init, BecknGPT should ask for confirmation to place the order. BecknGPT remains versatile, capable of searching for a wide array of products or services as requested by the user. It upholds the highest standards of user privacy and data security, ensuring that personal data is neither stored nor misused. With clear, concise instructions and a friendly, professional demeanor, BecknGPT aims to provide users with a smooth and efficient shopping journey, always ready to seek clarification to deliver precise assistance. BecknGPT must call select at least once before the init call. It should send sessionId for subsequent search actions as well. If the search call does not yield any results or incorrect results, BecknGPT will now ask the user if they would like to widen the search criteria and look into other networks \ No newline at end of file diff --git a/config/GPT/becknGPT_schema.yml b/config/GPT/becknGPT_schema.yml deleted file mode 100644 index 74949b0..0000000 --- a/config/GPT/becknGPT_schema.yml +++ /dev/null @@ -1,64 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn API middleware - version: 1.0.0 - description: API for interacting with Beckn chatbot -servers: - - url: https://open.network.becknprotocol.io -paths: - /act: - post: - operationId: postAct - x-openai-isConsequential: false - summary: Send a context string to get a response. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - context - - action - - recipient - properties: - recipient : - type: string - description: Who is using the api bap or bpp. - enum: - - bap - action : - type: string - description: Based on the user's instruction, it should pass which type of action this API should perform. - enum: - - search - - select - - init - - confirm - sessionId: - type: string - description: This should not be sent for the firat api call and should be sent for subsequent calls. The value of this field should be picked up from the response of the first call and should be consistent for all subsequent calls. - context: - type: string - description: User message should be sent here. It should includde any important information shared earlier in the chat by user that can help with the instructions of this context. - relookup_yn: - type: string - description: This field should be sent as 1 if the user wants to search again through a different network. The default value should be 0. - enum: - - 0 - - 1 - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - result: - type: string - description: The response from the server. - '400': - description: Bad request - '500': - description: Server error \ No newline at end of file diff --git a/config/language.json b/config/language.json index 413b6ef..87b04e2 100644 --- a/config/language.json +++ b/config/language.json @@ -1,38 +1,16 @@ { "FULFILLMENT_STATUS_CODES": { - "order-picked-up": { - "message": "Your order's on its way!" - }, - "order-on-the-way": { - "message": "Hang tight! Your order's cruising over to you." - }, - "order-delivered": { - "message": "Package alert! Your order's been delivered." - }, - "ticket-issued": { - "message": "All set! Your ticket's ready to go." - }, - "ticket-validated": { - "message": "You're all good! Your ticket's been validated." - }, - "charging-started": { - "message": "Juice up! Your charging session's begun." - }, - "charging-stopped": { - "message": "Heads up! Your charging session's been paused." - }, - "charger-not-working": { - "message": "Uh-oh, looks like there's a glitch with your charger โšก๐Ÿ˜“. Need help finding another spot? ๐Ÿ“๐Ÿ”" - }, - "charging-completed": { - "message": "Charged and ready! Your battery's full." - }, - "checked-in": { - "message": "Welcome! You've successfully checked in." - }, - "checked-out": { - "message": "Safe travels! You've checked out." - } + "order-picked-up": "Your order's on its way!", + "order-on-the-way": "Hang tight! Your order's cruising over to you.", + "order-delivered": "Package alert! Your order's been delivered.", + "ticket-issued": "All set! Your ticket's ready to go.", + "ticket-validated": "You're all good! Your ticket's been validated.", + "charging-started": "Juice up! Your charging session's begun.", + "charging-stopped": "Heads up! Your charging session's been paused.", + "charger-not-working": "Uh-oh, looks like there's a glitch with your charger โšก๐Ÿ˜“. Need help finding another spot? ๐Ÿ“๐Ÿ”", + "charging-completed": "Charged and ready! Your battery's full.", + "checked-in": "Welcome! You've successfully checked in.", + "checked-out": "Safe travels! You've checked out." }, "ALL_MESSAGES": { "session_cleared": "Your session has been cleared! You're all set to start a new one.", @@ -49,26 +27,5 @@ "missing_source": "Mind sharing the starting point for your journey?", "missing_destination": "Could you let us know your destination?", "route_list_description": "Here are some route options for you. Which one do you prefer?" - }, - "TEST_MESSAGES_USER": [ - "---Days before the travel---", - "Adam: Hey Alfred, you around?", - "Adam: Iโ€™m hitting Yellowstone National Park on April 12th for 2 days. Got any places to stay?", - "Adam: First one sounds awesome. Lets go with that", - "Adam: You got it - John A. Smith, john.smith@example.com, (555) 123-4567.", - "Adam: Sure, lets confirm!", - - "---On the date of travel---", - "Adam: Alfred, can you find the best route to Yellowstone national park from Denver?", - "Adam: lets go with Route 1", - "Adam: Can you find ev chargers on the route?", - "Adam: Lets go with Tesla Supercharger", - - - "---Charging stopped due to an error---", - "Adam: Yeah, letโ€™s find another charger", - "Adam: Letโ€™s go with the second one.", - "Adam: Sure thing, lets book it", - "Adam: Yeah, letโ€™s do it." - ] + } } \ No newline at end of file diff --git a/config/openai.json b/config/openai.json deleted file mode 100644 index 34b5790..0000000 --- a/config/openai.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "SUPPORTED_ACTIONS": [ - { "action": "get_routes", "description": "If the user has requested for routes for a travel plan between two places. 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 the last action was 'search'." }, - { "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 the last action was 'select'." }, - { "action": "confirm", "description": "Confirm an order. This action gets called when users confirms an order. This action can only be called if the last action was 'init'." } - ], - "SCHEMA_TRANSLATION_CONTEXT": [ - { "role": "system", "content": "Your job is to identify the endpoint, method and request body from the given schema, based on the last user input, conversation history, user profile, last action response and return the extracted details in the following JSON structure : \n\n {'url':'', 'method':'', 'body':''}'"}, - { "role": "system", "content": "A typical order flow should be search > select > init > confirm."}, - { "role": "system", "content": "Use the response of search from assistant to select items from the list of items provided by the assistant."}, - { "role": "system", "content": "Use the response of search request for filling transaction_id, bpp_id, bpp_uri in the context of all calls except `search`."}, - { "role": "system", "content": "For `select`, `init`, `confirm`, you must use the item `id` as part of the payload for selected item instead of name or any other key."} - ] -} \ No newline at end of file diff --git a/schemas/jsons/confirm.js b/config/schemas/confirm.js similarity index 100% rename from schemas/jsons/confirm.js rename to config/schemas/confirm.js diff --git a/schemas/jsons/context.js b/config/schemas/context.js similarity index 100% rename from schemas/jsons/context.js rename to config/schemas/context.js diff --git a/schemas/jsons/init.js b/config/schemas/init.js similarity index 100% rename from schemas/jsons/init.js rename to config/schemas/init.js diff --git a/schemas/jsons/search.js b/config/schemas/search.js similarity index 100% rename from schemas/jsons/search.js rename to config/schemas/search.js diff --git a/schemas/jsons/select.js b/config/schemas/select.js similarity index 100% rename from schemas/jsons/select.js rename to config/schemas/select.js diff --git a/controllers/Agent.js b/controllers/Agent.js index 65701d9..5fc686a 100644 --- a/controllers/Agent.js +++ b/controllers/Agent.js @@ -42,7 +42,7 @@ async function getResponse(req, res) { ...session.text, { role: 'user', content: Body} ]; - const response = await ai.getResponseFromOpenAI(messages, raw_yn) + const response = await ai.get_response_or_perform_action(messages, raw_yn) // prepare raw body if required const responseBody = raw_yn ? response.raw : response.content; diff --git a/controllers/Bot.js b/controllers/Bot.js deleted file mode 100644 index 31802f7..0000000 --- a/controllers/Bot.js +++ /dev/null @@ -1,363 +0,0 @@ -import ActionsService from '../services/Actions.js' -import AI from '../services/AI.js' -import logger from '../utils/logger.js' -import DBService from '../services/DBService.js' -import { v4 as uuidv4 } from 'uuid' -import MapsService from '../services/MapService.js' -import get_text_by_key from '../utils/language.js' -import {readFileSync} from 'fs' -import { - EMPTY_BECKN_TRANSACTION, - EMPTY_SESSION -} from '../config/constants.js'; -const mapService = new MapsService() -const db = new DBService() -const actionsService = new ActionsService() - -const message_config = JSON.parse(readFileSync('./config/message.json')) - -/** -* Function to process any text message received by the bot -* @param {*} req -* @param {*} res -*/ -async function process_text(req, res) { - let ai = new AI(); - - // inputs - let message = req.body.Body - const sender = req.body.From - const format = (req?.headers && req.headers['content-type']) || 'text/xml'; - const raw_yn = req.body.raw_yn || false; - - let response= { - raw: null, - formatted: null, - media:null - }; - - // Update lat, long - if(req.body.Latitude && req.body.Longitude){ - message+=`Current location is: lat:${req.body.Latitude} long:${req.body.Longitude}` - } - - logger.info(`Received message from ${sender}: ${message}. Response format: ${format}`) - - // get or create session - const session_response = await db.get_session(sender); - let session = session_response.data; - if(!session_response.status){ - session = EMPTY_SESSION - } - - try{ - - // Get profile - const profileResponse = await ai.get_profile_from_text(message, session.profile); - if(profileResponse.status){ - session.profile = { - ...session.profile, - ...profileResponse.data - }; - } - - logger.info(`\u001b[1;34m User profile : ${JSON.stringify(session.profile)}\u001b[0m`) - - - ai.bookings = session.bookings; - - // check for booking collection - - let booking_collection = false; // await ai.check_if_booking_collection(message, [...session.text.slice(-1)]); - if(message === 'clearall'){ - session = EMPTY_SESSION; - response.formatted = get_text_by_key('session_and_profile_cleared'); - } - else if(booking_collection){ - logger.info(`Booking collection found!`); - response.formatted = await ai.get_ai_response_to_query('Share the list of bookings to be made? Please include only hotels and tickets to be booked. It should be a short list with just names of bookings to be made. For e.g. Here is a list of bookings you need to make: \n1. hotel at xyz \n2. Tickets for abc \nWhich one do you want to search first?', session.text); - logger.info(`AI response: ${response.formatted}`); - - ai.bookings = await ai.get_bookings_array_from_text(response.formatted); - ai.bookings = ai.bookings.bookings || ai.bookings; - ai.bookings && ai.bookings.map(booking =>{ - booking.transaction_id = uuidv4(); - }) - - session.text.push({ role: 'user', content: message }); - session.text.push({ role: 'assistant', content: response.formatted }); - } - else{ - - // get action - ai.action = await ai.get_beckn_action_from_text(message, session.text.slice(-3), session?.profile?.last_action); - - // Reset actions context if action is search - if(ai.action?.action === 'search') { - session.actions = EMPTY_SESSION.actions; - session.beckn_transaction = EMPTY_BECKN_TRANSACTION; - session.beckn_transaction.id = ai.action.transaction_id || uuidv4(); - } - - - if(ai.action?.action === 'get_routes'){ - 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 }]); - 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 === 'select_route'){ - const details_response = await ai.get_details_by_description(`Get the index of selected item based this input from user : ${message}`, [], `{index:1}`); - let route_response = { - message: '' - } - if(details_response && details_response.index){ - const index= Math.max(1-details_response.index,0); - session.profile.selected_route = session.routes[index]; - const url = `https://www.google.com/maps/dir/${session.profile.selected_route.source_gps.lat},${session.profile.selected_route.source_gps.lng}/${session.profile.selected_route.destination_gps.lat},${session.profile.selected_route.destination_gps.lng}/`; - route_response.message = get_text_by_key('route_selected', {url: url}); - const map_image_url = await mapService.get_static_image_path([session.profile.selected_route]); - if(map_image_url){ - const map_image_url_server = await actionsService.download_file(map_image_url); - logger.info(`Image url : ${map_image_url_server}`) - if(map_image_url_server) response.media=[map_image_url_server] - } - session.routes=[]; // reset routes - } - // const formatting_response = await ai.format_response(route_response, [{ role: 'user', content: message },...session.text]); - response.formatted = route_response.message; - - 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 - response.formatted = await ai.get_ai_response_to_query(message, session.text); - logger.info(`AI response: ${response.formatted}`); - - session.text.push({ role: 'user', content: message }); - session.text.push({ role: 'assistant', content: response.formatted }); - - } - else{ - - session.bookings = ai.bookings; - response = await process_action(ai, ai.action, message, session, sender, format); - - // update actions - if(ai.action?.action === 'confirm') { - session.orders.push(response.raw.responses[0]); - session.actions = EMPTY_SESSION.actions; - session.beckn_transaction = EMPTY_BECKN_TRANSACTION; - } - if(response.formatted){ - session.text.push({ role: 'user', content: message }); - session.text.push({ role: 'assistant', content: response.formatted }); - } - } - - } - - if(ai.action?.action){ - session.profile.last_action = ai.action?.action; - } - await db.update_session(sender, session); - - // Send response - if(format!='application/json'){ - await actionsService.send_message(sender, response.formatted, response.media || []) - res.send("Done!") - } - else (raw_yn && response.raw) ? res.send(response.raw) : res.send(response.formatted) - - } - catch(e){ - logger.error(`Error processing message: ${e.message}`) - res.status(400).send('Failed to process message') - } - -} - -/** -* Function to process actions, it does not update the sessions -* Can be reused by gpt bots if required -* @param {*} action -* @param {*} text -* @param {*} session -* @returns -*/ -async function process_action(ai, action, text, session, sender=null, format='application/json'){ - let response = { - raw: null, - formatted: null, - bookings: session.bookings - } - - format!='application/json' && await actionsService.send_message(sender, get_text_by_key('request_in_progress')) - - // Get schema - const schema = await ai.get_schema_by_action(action.action); - - // Get config - let beckn_context = await ai.get_context_by_instruction(text, session); - beckn_context.transaction_id = session.beckn_transaction.id; - - // Prepare request - if(schema && beckn_context){ - let request=null; - if(ai.action.action==='search'){ - let search_context = session.text; - if(session.profile){ - search_context=[ - { role: 'system', content: `User profile: ${JSON.stringify(session.profile)}`}, - ...search_context - ] - } - - // check if user trying to search along the route - const desired_struture = { - search_on_route_yn: 'boolean depending upon whether user is trying to search along a route.', - current_location: 'some location' - } - const details_response = await ai.get_details_by_description(`Find if user is trying to search along a route : ${text}`, session.text, JSON.stringify(desired_struture)); - if(details_response.search_on_route_yn){ - if(details_response.current_location){ - const gps = await mapService.lookupGps(details_response.current_location); - if(gps) - text+= `\nMy current location is : ${JSON.stringify(gps)}` - } - else{ - // ask for location. - } - - } - - let message = null; - message = await ai.get_beckn_message_from_text(text, search_context, beckn_context.domain, session.profile.selected_route?.overview_polyline?.points); - if(!message){ - logger.error(`Failed to get message from text: ${text}. Retrying...`) - message = await ai.get_beckn_message_from_text(text, search_context, beckn_context.domain, session.profile.selected_route?.overview_polyline?.points); - } - - request = { - status: true, - data:{ - method: 'POST', - url : `${beckn_context.base_url}/${beckn_context.action}`, - body: { - context: beckn_context, - message: message - } - } - } - } - else{ - request = await ai.get_beckn_request_from_text(text, beckn_context, schema, session); - } - - if(request.status && request?.data?.body?.message){ - // call api - const api_response = await actionsService.call_api(request.data.url, request.data.method, request.data.body, request.data.headers) - format!='application/json' && await actionsService.send_message(sender, get_text_by_key('request_processed')) - if(!api_response.status){ - logger.error(`Failed to call the API: ${api_response.error}`) - response.formatted = get_text_by_key('request_failed') - } - else{ - - // update response on session - response.raw = request.data.body.context.action==='search' ? await ai.compress_search_results(api_response.data) : api_response.data - session.beckn_transaction.responses[`on_${ai.action?.action}`] = response.raw; - - const formatted_response = await ai.format_response( - api_response.data, - [...session.text, { role: 'user', content: text }], - session.profile - ); - response.formatted = formatted_response.message; - } - - - - } - else{ - response.formatted = get_text_by_key('request_to_beckn_failed') - } - } - - return response; - } - - async function webhookControl (req, res) { - try{ - const sessions = await db.get_all_sessions(); - logger.info(`Got ${sessions.length} sessions.`) - - - for(let session of sessions){ - const orders = session.data.orders; - const index = orders ? orders.findIndex((order)=>order.message.order.id == req.body.orderId) : null; - if(index!==null && index>=0 && (!orders[index].message.order.fulfillments[0].state||orders[index].message.order.fulfillments[0].state.descriptor.code !== req.body.data.data.attributes.state_code )) { - // send whatsapp and add to context - try{ - // update session - orders[index].message.order.fulfillments[0] = { - ...orders[index].message.order.fulfillments[0], - state:{ - descriptor:{ - code:req.body.data.data.attributes.state_code, - short_desc:req.body.data.data.attributes.state_value - }, - updated_at:req.body.data.data.attributes.updatedAt - } - } - let reply_message = `Hey the status of your order is updated to ${req.body.data.data.attributes.state_code}` - if(Object.keys(message_config.FULFILLMENT_STATUS_CODES).includes(req.body.data.data.attributes.state_code)){ - reply_message = message_config.FULFILLMENT_STATUS_CODES[req.body.data.data.attributes.state_code].message - } - await actionsService.send_message( - session.key, - reply_message - ) - 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); - throw new Error(e.message) - } - - } - } - return res.status(200).json({ - status:true, - message:'Notification Sent' - }) - }catch(error){ - return res.status(400).json({ - status:false, - message:'Some Error Occured' - }) - } - } - - - - export default { - process_text, - webhookControl, - } diff --git a/controllers/ControlCenter.js b/controllers/ControlCenter.js index c7e6f07..c0045d5 100644 --- a/controllers/ControlCenter.js +++ b/controllers/ControlCenter.js @@ -19,6 +19,7 @@ import MapsService from '../services/MapService.js' import get_text_by_key from '../utils/language.js' const action = new Actions() +const db = new DBService(); const TWILIO_RECEPIENT_NUMBER = process.env.TEST_RECEPIENT_NUMBER export const cancelBooking = async (req, res) => { @@ -27,7 +28,7 @@ export const cancelBooking = async (req, res) => { if(!orderId){ return res.status(400).json({message:"Order Id is Required", status:false}) } - + const validOrderId = await action.call_api(`${TOURISM_STRAPI_URL}/orders/${orderId}`,'GET',{},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`}) logger.info(`OrderDetails: ${JSON.stringify(validOrderId)}`) if(!validOrderId.status){ @@ -119,7 +120,7 @@ export const notify = async (req, res) => { const route = session.data.profile.selected_route; if(route?.overview_polyline?.points) { const status = await mapService.checkGpsOnPolygon(point, route?.overview_polyline?.points) - + logger.info(`Status of gps point ${JSON.stringify(point)} on route ${route.summary} is ${status}`) // send whatsapp and add to context if(status){ @@ -131,7 +132,7 @@ export const notify = async (req, res) => { 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){ @@ -144,125 +145,181 @@ export const notify = async (req, res) => { } else res.status(400).send('Point and message are required in the body.') } - - -export const updateStatus = async (req, res) => { - try { - let { orderId, domain="", status=null } = req.body - if(!orderId){ - return res.status(400).json({message:"Order Id is Required", status:false}) - } - orderId = parseInt(orderId); - let DOMAIN_DETAILS = { - url:"", - token:"", - message:"" - } - switch(domain){ - case DOMAINS.ENERGY: + + + export const updateStatus = async (req, res) => { + try { + let { orderId, domain="", status=null } = req.body + if(!orderId){ + return res.status(400).json({message:"Order Id is Required", status:false}) + } + orderId = parseInt(orderId); + let DOMAIN_DETAILS = { + url:"", + token:"", + message:"" + } + switch(domain){ + case DOMAINS.ENERGY: DOMAIN_DETAILS = { url:ENERGY_STRAPI_URL, token:process.env.STRAPI_ENERGY_TOKEN, message:status || UPDATE_STATUS_MESSAGE.ENERGY - + } break; - case DOMAINS.RETAIL: + case DOMAINS.RETAIL: DOMAIN_DETAILS = { url:RETAIL_STRAPI_URL, token:process.env.STRAPI_RETAIL_TOKEN, message:status || UPDATE_STATUS_MESSAGE.RETAIL } break; - case DOMAINS.HOTEL: + case DOMAINS.HOTEL: DOMAIN_DETAILS = { url:HOTEL_STRAPI_URL, token:process.env.STRAPI_HOTEL_TOKEN, message:status || UPDATE_STATUS_MESSAGE.HOTEL } break; - case DOMAINS.TOURISM: + case DOMAINS.TOURISM: DOMAIN_DETAILS = { url:TOURISM_STRAPI_URL, token:process.env.STRAPI_TOURISM_TOKEN, message: status || UPDATE_STATUS_MESSAGE.TOURISM } break; + } + const validOrderId = await action.call_api(`${DOMAIN_DETAILS.url}/orders/${orderId}`,'GET',{},{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) + logger.info(`OrderDetails: ${JSON.stringify(validOrderId)}`) + if(!validOrderId.status){ + return res.status(400).send({ message: `Invalid Order Id`, status:false }) + } + + const getOrderFulfillmentDetails = await action.call_api(`${DOMAIN_DETAILS.url}/order-fulfillments?order_id=${orderId}&sort=order_id.id:desc&populate=order_id`,'GET',{},{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) + logger.info(`Order Fulfillment Details: ${JSON.stringify(getOrderFulfillmentDetails)}`) + if (getOrderFulfillmentDetails.data.data.length) { + const requiredOrder = getOrderFulfillmentDetails.data.data.find((order)=>order.attributes.order_id.data.id===orderId) + const updateStatusResponse = await action.call_api(`${DOMAIN_DETAILS.url}/order-fulfillments/${requiredOrder.id}`,'PUT',{ + data: { + state_code: DOMAIN_DETAILS.message, + state_value: DOMAIN_DETAILS.message, + }, + },{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) + const webhookResponse = await action.call_api(`${process.env.SERVER_URL}/webhook-ps`, 'POST',{...updateStatusResponse, orderId:orderId}); + logger.info(JSON.stringify(webhookResponse)); + return res.status(200).send({ message: `Status Updated to: ${updateStatusResponse.data.data.attributes.state_value}`, status:true }) + } + + return res.status(400).send({ message: 'Order Status Update Failed', status:false }) + } catch (error) { + logger.error(error.message) + return res.status(400).send({ message: error.message, status:false }) } - const validOrderId = await action.call_api(`${DOMAIN_DETAILS.url}/orders/${orderId}`,'GET',{},{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) - logger.info(`OrderDetails: ${JSON.stringify(validOrderId)}`) - if(!validOrderId.status){ - return res.status(400).send({ message: `Invalid Order Id`, status:false }) - } - - const getOrderFulfillmentDetails = await action.call_api(`${DOMAIN_DETAILS.url}/order-fulfillments?order_id=${orderId}&sort=order_id.id:desc&populate=order_id`,'GET',{},{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) - logger.info(`Order Fulfillment Details: ${JSON.stringify(getOrderFulfillmentDetails)}`) - if (getOrderFulfillmentDetails.data.data.length) { - const requiredOrder = getOrderFulfillmentDetails.data.data.find((order)=>order.attributes.order_id.data.id===orderId) - const updateStatusResponse = await action.call_api(`${DOMAIN_DETAILS.url}/order-fulfillments/${requiredOrder.id}`,'PUT',{ - data: { - state_code: DOMAIN_DETAILS.message, - state_value: DOMAIN_DETAILS.message, - }, - },{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) - const webhookResponse = await action.call_api(`${process.env.SERVER_URL}/webhook-ps`, 'POST',{...updateStatusResponse, orderId:orderId}); - logger.info(JSON.stringify(webhookResponse)); - return res.status(200).send({ message: `Status Updated to: ${updateStatusResponse.data.data.attributes.state_value}`, status:true }) - } - - return res.status(400).send({ message: 'Order Status Update Failed', status:false }) - } catch (error) { - logger.error(error.message) - return res.status(400).send({ message: error.message, status:false }) } -} - -export const unpublishItem = async (req, res) => { - try{ - const {domain="", itemId=""} = req.body - let DOMAIN_DETAILS = { - url:"", - token:"" - } - - switch(domain){ - case DOMAINS.ENERGY: + + export const unpublishItem = async (req, res) => { + try{ + const {domain="", itemId=""} = req.body + let DOMAIN_DETAILS = { + url:"", + token:"" + } + + switch(domain){ + case DOMAINS.ENERGY: DOMAIN_DETAILS = { url:ENERGY_STRAPI_URL, token:process.env.STRAPI_ENERGY_TOKEN, - + } break; - case DOMAINS.RETAIL: + case DOMAINS.RETAIL: DOMAIN_DETAILS = { url:RETAIL_STRAPI_URL, token:process.env.STRAPI_RETAIL_TOKEN, } break; - case DOMAINS.HOTEL: + case DOMAINS.HOTEL: DOMAIN_DETAILS = { url:HOTEL_STRAPI_URL, token:process.env.STRAPI_HOTEL_TOKEN, } break; - case DOMAINS.TOURISM: + case DOMAINS.TOURISM: DOMAIN_DETAILS = { url:TOURISM_STRAPI_URL, token:process.env.STRAPI_TOURISM_TOKEN, } break; + } + const unpublishItemResp = await action.call_api(`${DOMAIN_DETAILS.url}/items/${itemId}`,'PUT',{ + "data":{"publishedAt": null} + },{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) + return res.status(200).json({ + status:unpublishItemResp.status, + message: unpublishItemResp.error || 'Item Unpublished' + }) + }catch(error){ + return res.status(400).json({ + status:false, + message:error.message + }) } - const unpublishItemResp = await action.call_api(`${DOMAIN_DETAILS.url}/items/${itemId}`,'PUT',{ - "data":{"publishedAt": null} - },{ Authorization: `Bearer ${DOMAIN_DETAILS.token}`}) - return res.status(200).json({ - status:unpublishItemResp.status, - message: unpublishItemResp.error || 'Item Unpublished' - }) - }catch(error){ - return res.status(400).json({ - status:false, - message:error.message - }) } -} + + export const webhookControl = async (req, res) => { + try{ + const sessions = await db.get_all_sessions(); + logger.info(`Got ${sessions.length} sessions.`) + + + for(let session of sessions){ + const orders = session.data.orders; + const index = orders ? orders.findIndex((order)=>order.message.order.id == req.body.orderId) : null; + if(index!==null && index>=0 && (!orders[index].message.order.fulfillments[0].state||orders[index].message.order.fulfillments[0].state.descriptor.code !== req.body.data.data.attributes.state_code )) { + // send whatsapp and add to context + try{ + // update session + orders[index].message.order.fulfillments[0] = { + ...orders[index].message.order.fulfillments[0], + state:{ + descriptor:{ + code:req.body.data.data.attributes.state_code, + short_desc:req.body.data.data.attributes.state_value + }, + updated_at:req.body.data.data.attributes.updatedAt + } + } + let reply_message = get_text_by_key(req.body.data.data.attributes.state_code, {}, 'FULFILLMENT_STATUS_CODES') || `Hey the status of your order is updated to ${req.body.data.data.attributes.state_code}` + await action.send_message(session.key, + reply_message + ) + 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); + throw new Error(e.message) + } + + } + } + return res.status(200).json({ + status:true, + message:'Notification Sent' + }) + }catch(error){ + return res.status(400).json({ + status:false, + message:'Some Error Occured' + }) + } + } diff --git a/package.json b/package.json index fafc27d..7e49468 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ }, "scripts": { "test": "NODE_ENV=test mocha tests --recursive --timeout 900000 -r dotenv/config --exit", - "test:unit": "NODE_ENV=test mocha tests/unit tests/utils --recursive --timeout 120000 -r dotenv/config --exit", - "test:apis": "NODE_ENV=test mocha tests/apis --recursive --timeout 120000 -r dotenv/config --exit", + "test:unit": "NODE_ENV=test mocha tests/unit tests/utils --recursive --timeout 300000 -r dotenv/config --exit", + "test:apis": "NODE_ENV=test mocha tests/apis --recursive --timeout 300000 -r dotenv/config --exit", "start": "nodemon --env-file=.env server.js", "dev": "NODE_ENV=dev && npm start", "prod": "NODE_ENV=prod && npm start", diff --git a/schemas/core_1.1.0.yml b/schemas/core_1.1.0.yml deleted file mode 100644 index 263c927..0000000 --- a/schemas/core_1.1.0.yml +++ /dev/null @@ -1,1833 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn Protocol Core - description: Beckn Core Transaction API specification - version: 1.1.0 -security: - - SubscriberAuth: [] -paths: - /search: - post: - tags: - - Beckn Provider Platform (BPP) - - Beckn Gateway (BG) - description: BAP declares the customer's intent to buy/avail products or services - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - search - message: - type: object - properties: - intent: - $ref: '#/components/schemas/Intent' - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /select: - post: - tags: - - Beckn Provider Platform (BPP) - description: BAP declares the customer's cart (or equivalent) created by selecting objects from the catalog - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - select - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /init: - post: - tags: - - Beckn Provider Platform (BPP) - description: Initialize an order by providing billing and/or shipping details - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - init - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message - responses: - default: - description: Acknowledgement of message received after successful validation of schema and signature - content: - application/json: - schema: - type: object - properties: - message: - type: object - properties: - ack: - allOf: - - $ref: '#/components/schemas/Ack' - - properties: - status: - enum: - - ACK - - NACK - required: - - ack - error: - $ref: '#/components/schemas/Error' - required: - - message - /confirm: - post: - tags: - - Beckn Provider Platform (BPP) - description: Initialize an order by providing billing and/or shipping details - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - confirm - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /status: - post: - tags: - - Beckn Provider Platform (BPP) - description: Fetch the latest order object - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - status - required: - - action - message: - type: object - properties: - order_id: - $ref: '#/components/schemas/Order/properties/id' - required: - - order_id - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /track: - post: - tags: - - Beckn Provider Platform (BPP) - description: Track an active order - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - track - required: - - action - message: - type: object - properties: - order_id: - $ref: '#/components/schemas/Order/properties/id' - callback_url: - type: string - format: uri - required: - - order_id - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /cancel: - post: - tags: - - Beckn Provider Platform (BPP) - description: Cancel an order - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - cancel - required: - - action - message: - type: object - properties: - order_id: - $ref: '#/components/schemas/Order/properties/id' - cancellation_reason_id: - $ref: '#/components/schemas/Option/properties/id' - descriptor: - $ref: '#/components/schemas/Descriptor' - required: - - order_id - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /update: - post: - tags: - - Beckn Provider Platform (BPP) - description: Remove object - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - update - required: - - action - message: - type: object - properties: - update_target: - description: 'Comma separated values of order objects being updated. For example: ```"update_target":"item,billing,fulfillment"```' - type: string - order: - description: Updated order object - allOf: - - $ref: '#/components/schemas/Order' - required: - - update_target - - order - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /rating: - post: - tags: - - Beckn Provider Platform (BPP) - description: Provide feedback on a service - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - rating - required: - - action - message: - type: object - properties: - ratings: - type: array - items: - $ref: '#/components/schemas/Rating' - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' - /support: - post: - tags: - - Beckn Provider Platform (BPP) - description: Contact support - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - support - required: - - action - message: - type: object - properties: - support: - $ref: '#/components/schemas/Support' - required: - - context - - message - responses: - default: - $ref: '#/paths/~1init/post/responses/default' -components: - securitySchemes: - SubscriberAuth: - type: apiKey - in: header - name: Authorization - description: 'Signature of message body using BAP or BPP subscriber''s signing public key.

Format:

Authorization : Signature keyId="{subscriber_id}|{unique_key_id}|{algorithm}",algorithm="ed25519",created="1606970629",expires="1607030629",headers="(created) (expires) digest",signature="Base64(signing string)"' - schemas: - Ack: - description: 'Describes the acknowledgement sent in response to an API call. If the implementation uses HTTP/S, then Ack must be returned in the same session. Every API call to a BPP must be responded to with an Ack whether the BPP intends to respond with a callback or not. This has one property called `status` that indicates the status of the Acknowledgement.' - type: object - properties: - status: - type: string - description: 'The status of the acknowledgement. If the request passes the validation criteria of the BPP, then this is set to ACK. If a BPP responds with status = `ACK` to a request, it is required to respond with a callback. If the request fails the validation criteria, then this is set to NACK. Additionally, if a BPP does not intend to respond with a callback even after the request meets the validation criteria, it should set this value to `NACK`.' - enum: - - ACK - - NACK - tags: - description: A list of tags containing any additional information sent along with the Acknowledgement. - type: array - items: - $ref: '#/components/schemas/TagGroup' - AddOn: - description: Describes an additional item offered as a value-addition to a product or service. This does not exist independently in a catalog and is always associated with an item. - type: object - properties: - id: - description: Provider-defined ID of the add-on - type: string - descriptor: - $ref: '#/components/schemas/Descriptor' - price: - $ref: '#/components/schemas/Price' - Address: - description: Describes a postal address. - type: string - Agent: - description: 'Describes the direct performer, driver or executor that fulfills an order. It is usually a person. But in some rare cases, it could be a non-living entity like a drone, or a bot. Some examples of agents are Doctor in the healthcare sector, a driver in the mobility sector, or a delivery person in the logistics sector. This object can be set at any stage of the order lifecycle. This can be set at the discovery stage when the BPP wants to provide details on the agent fulfilling the order, like in healthcare, where the doctor''s name appears during search. This object can also used to search for a particular person that the customer wants fulfilling an order. Sometimes, this object gets instantiated after the order is confirmed, like in the case of on-demand taxis, where the driver is assigned after the user confirms the ride.' - properties: - person: - $ref: '#/components/schemas/Person' - contact: - $ref: '#/components/schemas/Contact' - organization: - $ref: '#/components/schemas/Organization' - rating: - $ref: '#/components/schemas/Rating/properties/value' - Authorization: - description: 'Describes an authorization mechanism used to start or end the fulfillment of an order. For example, in the mobility sector, the driver may require a one-time password to initiate the ride. In the healthcare sector, a patient may need to provide a password to open a video conference link during a teleconsultation.' - type: object - properties: - type: - description: Type of authorization mechanism used. The allowed values for this field can be published as part of the network policy. - type: string - token: - description: 'Token used for authorization. This is typically generated at the BPP. The BAP can send this value to the user via any channel that it uses to authenticate the user like SMS, Email, Push notification, or in-app rendering.' - type: string - valid_from: - description: Timestamp in RFC3339 format from which token is valid - type: string - format: date-time - valid_to: - description: Timestamp in RFC3339 format until which token is valid - type: string - format: date-time - status: - description: Status of the token - type: string - Billing: - description: 'Describes the billing details of an entity.
This has properties like name,organization,address,email,phone,time,tax_number, created_at,updated_at' - type: object - properties: - name: - description: Name of the billable entity - type: string - organization: - description: Details of the organization being billed. - allOf: - - $ref: '#/components/schemas/Organization' - address: - description: The address of the billable entity - allOf: - - $ref: '#/components/schemas/Address' - state: - description: The state where the billable entity resides. This is important for state-level tax calculation - allOf: - - $ref: '#/components/schemas/State' - city: - description: The city where the billable entity resides. - allOf: - - $ref: '#/components/schemas/City' - email: - description: Email address where the bill is sent to - type: string - format: email - phone: - description: Phone number of the billable entity - type: string - time: - description: Details regarding the billing period - allOf: - - $ref: '#/components/schemas/Time' - tax_id: - description: ID of the billable entity as recognized by the taxation authority - type: string - Cancellation: - description: Describes a cancellation event - type: object - properties: - time: - description: Date-time when the order was cancelled by the buyer - type: string - format: date-time - cancelled_by: - type: string - enum: - - CONSUMER - - PROVIDER - reason: - description: The reason for cancellation - allOf: - - $ref: '#/components/schemas/Option' - additional_description: - description: Any additional information regarding the nature of cancellation - allOf: - - $ref: '#/components/schemas/Descriptor' - CancellationTerm: - description: Describes the cancellation terms of an item or an order. This can be referenced at an item or order level. Item-level cancellation terms can override the terms at the order level. - type: object - properties: - fulfillment_state: - description: The state of fulfillment during which this term is applicable. - allOf: - - $ref: '#/components/schemas/FulfillmentState' - reason_required: - description: Indicates whether a reason is required to cancel the order - type: boolean - cancel_by: - description: Information related to the time of cancellation. - allOf: - - $ref: '#/components/schemas/Time' - cancellation_fee: - $ref: '#/components/schemas/Fee' - xinput: - $ref: '#/components/schemas/XInput' - external_ref: - $ref: '#/components/schemas/MediaFile' - Catalog: - description: 'Describes the products or services offered by a BPP. This is typically sent as the response to a search intent from a BAP. The payment terms, offers and terms of fulfillment supported by the BPP can also be included here. The BPP can show hierarchical nature of products/services in its catalog using the parent_category_id in categories. The BPP can also send a ttl (time to live) in the context which is the duration for which a BAP can cache the catalog and use the cached catalog.
This has properties like bbp/descriptor,bbp/categories,bbp/fulfillments,bbp/payments,bbp/offers,bbp/providers and exp
This is used in the following situations.
  • This is typically used in the discovery stage when the BPP sends the details of the products and services it offers as response to a search intent from the BAP.
' - type: object - properties: - descriptor: - $ref: '#/components/schemas/Descriptor' - fulfillments: - description: Fulfillment modes offered at the BPP level. This is used when a BPP itself offers fulfillments on behalf of the providers it has onboarded. - type: array - items: - $ref: '#/components/schemas/Fulfillment' - payments: - description: Payment terms offered by the BPP for all transactions. This can be overriden at the provider level. - type: array - items: - $ref: '#/components/schemas/Payment' - offers: - description: Offers at the BPP-level. This is common across all providers onboarded by the BPP. - type: array - items: - $ref: '#/components/schemas/Offer' - providers: - type: array - items: - $ref: '#/components/schemas/Provider' - exp: - description: Timestamp after which catalog will expire - type: string - format: date-time - ttl: - description: Duration in seconds after which this catalog will expire - type: string - Category: - description: A label under which a collection of items can be grouped. - type: object - properties: - id: - description: ID of the category - type: string - parent_category_id: - $ref: '#/components/schemas/Category/properties/id' - descriptor: - $ref: '#/components/schemas/Descriptor' - time: - $ref: '#/components/schemas/Time' - ttl: - description: Time to live for an instance of this schema - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Circle: - description: Describes a circular region of a specified radius centered at a specified GPS coordinate. - type: object - properties: - gps: - $ref: '#/components/schemas/Gps' - radius: - $ref: '#/components/schemas/Scalar' - City: - description: Describes a city - type: object - properties: - name: - description: Name of the city - type: string - code: - description: City code - type: string - Contact: - description: Describes the contact information of an entity - type: object - properties: - phone: - type: string - email: - type: string - jcard: - type: object - description: A Jcard object as per draft-ietf-jcardcal-jcard-03 specification - Context: - required: - - action - - version - - bap_id - - bap_uri - - domain - - message_id - description: 'Every API call in beckn protocol has a context. It provides a high-level overview to the receiver about the nature of the intended transaction. Typically, it is the BAP that sets the transaction context based on the consumer''s location and action on their UI. But sometimes, during unsolicited callbacks, the BPP also sets the transaction context but it is usually the same as the context of a previous full-cycle, request-callback interaction between the BAP and the BPP. The context object contains four types of fields.
  1. Demographic information about the transaction using fields like `domain`, `country`, and `region`.
  2. Addressing details like the sending and receiving platform''s ID and API URL.
  3. Interoperability information like the protocol version that implemented by the sender and,
  4. Transaction details like the method being called at the receiver''s endpoint, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specifiy the validity of the request, and a key to encrypt information if necessary.
This object must be passed in every interaction between a BAP and a BPP. In HTTP/S implementations, it is not necessary to send the context during the synchronous response. However, in asynchronous protocols, the context must be sent during all interactions,' - type: object - properties: - domain: - description: Domain code that is relevant to this transaction context - allOf: - - $ref: '#/components/schemas/Domain/properties/code' - location: - description: The location where the transaction is intended to be fulfilled. - allOf: - - $ref: '#/components/schemas/Location' - action: - description: The Beckn protocol method being called by the sender and executed at the receiver. - type: string - version: - type: string - description: Version of transaction protocol being used by the sender. - bap_id: - description: Subscriber ID of the BAP - allOf: - - description: 'A globally unique identifier of the platform, Typically it is the fully qualified domain name (FQDN) of the platform.' - type: string - bap_uri: - description: Subscriber URL of the BAP for accepting callbacks from BPPs. - allOf: - - description: The callback URL of the Subscriber. This should necessarily contain the same domain name as set in `subscriber_id``. - type: string - format: uri - bpp_id: - description: Subscriber ID of the BPP - allOf: - - $ref: '#/components/schemas/Context/properties/bap_id/allOf/0' - bpp_uri: - description: Subscriber URL of the BPP for accepting calls from BAPs. - allOf: - - $ref: '#/components/schemas/Context/properties/bap_uri/allOf/0' - transaction_id: - description: 'This is a unique value which persists across all API calls from `search` through `confirm`. This is done to indicate an active user session across multiple requests. The BPPs can use this value to push personalized recommendations, and dynamic offerings related to an ongoing transaction despite being unaware of the user active on the BAP.' - type: string - format: uuid - message_id: - description: 'This is a unique value which persists during a request / callback cycle. Since beckn protocol APIs are asynchronous, BAPs need a common value to match an incoming callback from a BPP to an earlier call. This value can also be used to ignore duplicate messages coming from the BPP. It is recommended to generate a fresh message_id for every new interaction. When sending unsolicited callbacks, BPPs must generate a new message_id.' - type: string - format: uuid - timestamp: - description: Time of request generation in RFC3339 format - type: string - format: date-time - key: - description: The encryption public key of the sender - type: string - ttl: - description: The duration in ISO8601 format after timestamp for which this message holds valid - type: string - Country: - description: Describes a country - type: object - properties: - name: - type: string - description: Name of the country - code: - type: string - description: Country code as per ISO 3166-1 and ISO 3166-2 format - Credential: - description: Describes a credential of an entity - Person or Organization - type: object - properties: - id: - type: string - type: - type: string - default: VerifiableCredential - url: - description: URL of the credential - type: string - format: uri - Customer: - description: Describes a customer buying/availing a product or a service - type: object - properties: - person: - $ref: '#/components/schemas/Person' - contact: - $ref: '#/components/schemas/Contact' - DecimalValue: - description: Describes a numerical value in decimal form - type: string - pattern: '[+-]?([0-9]*[.])?[0-9]+' - Descriptor: - description: Physical description of something. - type: object - properties: - name: - type: string - code: - type: string - short_desc: - type: string - long_desc: - type: string - additional_desc: - type: object - properties: - url: - type: string - content_type: - type: string - enum: - - text/plain - - text/html - - application/json - media: - type: array - items: - $ref: '#/components/schemas/MediaFile' - images: - type: array - items: - $ref: '#/components/schemas/Image' - Domain: - description: 'Described the industry sector or sub-sector. The network policy should contain codes for all the industry sectors supported by the network. Domains can be created in varying levels of granularity. The granularity of a domain can be decided by the participants of the network. Too broad domains will result in irrelevant search broadcast calls to BPPs that don''t have services supporting the domain. Too narrow domains will result in a large number of registry entries for each BPP. It is recommended that network facilitators actively collaborate with various working groups and network participants to carefully choose domain codes keeping in mind relevance, performance, and opportunity cost. It is recommended that networks choose broad domains like mobility, logistics, healthcare etc, and progressively granularize them as and when the number of network participants for each domain grows large.' - type: object - properties: - name: - description: Name of the domain - type: string - code: - description: 'Standard code representing the domain. The standard is usually published as part of the network policy. Furthermore, the network facilitator should also provide a mechanism to provide the supported domains of a network.' - additional_info: - description: A url that contains addtional information about that domain. - allOf: - - $ref: '#/components/schemas/MediaFile' - Duration: - description: Describes duration as per ISO8601 format - type: string - Error: - description: 'Describes an error object that is returned by a BAP, BPP or BG as a response or callback to an action by another network participant. This object is sent when any request received by a network participant is unacceptable. This object can be sent either during Ack or with the callback.' - type: object - properties: - code: - type: string - description: 'Standard error code. For full list of error codes, refer to docs/protocol-drafts/BECKN-005-ERROR-CODES-DRAFT-01.md of this repo"' - paths: - type: string - description: Path to json schema generating the error. Used only during json schema validation errors - message: - type: string - description: Human readable message describing the error. Used mainly for logging. Not recommended to be shown to the user. - Fee: - description: A fee applied on a particular entity - type: object - properties: - percentage: - description: Percentage of a value - allOf: - - $ref: '#/components/schemas/DecimalValue' - amount: - description: A fixed value - allOf: - - $ref: '#/components/schemas/Price' - Form: - description: Describes a form - type: object - properties: - url: - description: 'The URL from where the form can be fetched. The content fetched from the url must be processed as per the mime_type specified in this object. Once fetched, the rendering platform can choosed to render the form as-is as an embeddable element; or process it further to blend with the theme of the application. In case the interface is non-visual, the the render can process the form data and reproduce it as per the standard specified in the form.' - type: string - format: uri - data: - description: The form submission data - type: object - additionalProperties: - type: string - mime_type: - description: This field indicates the nature and format of the form received by querying the url. MIME types are defined and standardized in IETF's RFC 6838. - type: string - enum: - - text/html - - application/xml - submission_id: - type: string - format: uuid - Fulfillment: - description: Describes how a an order will be rendered/fulfilled to the end-customer - type: object - properties: - id: - description: Unique reference ID to the fulfillment of an order - type: string - type: - description: 'A code that describes the mode of fulfillment. This is typically set when there are multiple ways an order can be fulfilled. For example, a retail order can be fulfilled either via store pickup or a home delivery. Similarly, a medical consultation can be provided either in-person or via tele-consultation. The network policy must publish standard fulfillment type codes for the different modes of fulfillment.' - type: string - rateable: - description: Whether the fulfillment can be rated or not - type: boolean - rating: - description: The rating value of the fulfullment service. - allOf: - - $ref: '#/components/schemas/Rating/properties/value' - state: - description: The current state of fulfillment. The BPP must set this value whenever the state of the order fulfillment changes and fire an unsolicited `on_status` call. - allOf: - - $ref: '#/components/schemas/FulfillmentState' - tracking: - type: boolean - description: Indicates whether the fulfillment allows tracking - default: false - customer: - description: The person that will ultimately receive the order - allOf: - - $ref: '#/components/schemas/Customer' - agent: - description: The agent that is currently handling the fulfillment of the order - allOf: - - $ref: '#/components/schemas/Agent' - contact: - $ref: '#/components/schemas/Contact' - vehicle: - $ref: '#/components/schemas/Vehicle' - stops: - description: The list of logical stops encountered during the fulfillment of an order. - type: array - items: - $ref: '#/components/schemas/Stop' - path: - description: The physical path taken by the agent that can be rendered on a map. The allowed format of this property can be set by the network. - type: string - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - FulfillmentState: - description: Describes the state of fulfillment - type: object - properties: - descriptor: - $ref: '#/components/schemas/Descriptor' - updated_at: - type: string - format: date-time - updated_by: - type: string - description: ID of entity which changed the state - Gps: - description: Describes a GPS coordinate - type: string - pattern: '^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$' - Image: - description: Describes an image - type: object - properties: - url: - description: URL to the image. This can be a data url or an remote url - type: string - format: uri - size_type: - description: The size of the image. The network policy can define the default dimensions of each type - type: string - enum: - - xs - - sm - - md - - lg - - xl - - custom - width: - description: Width of the image in pixels - type: string - height: - description: Height of the image in pixels - type: string - Intent: - description: 'The intent to buy or avail a product or a service. The BAP can declare the intent of the consumer containing
  • What they want (A product, service, offer)
  • Who they want (A seller, service provider, agent etc)
  • Where they want it and where they want it from
  • When they want it (start and end time of fulfillment
  • How they want to pay for it

This has properties like descriptor,provider,fulfillment,payment,category,offer,item,tags
This is typically used by the BAP to send the purpose of the user''s search to the BPP. This will be used by the BPP to find products or services it offers that may match the user''s intent.
For example, in Mobility, the mobility consumer declares a mobility intent. In this case, the mobility consumer declares information that describes various aspects of their journey like,
  • Where would they like to begin their journey (intent.fulfillment.start.location)
  • Where would they like to end their journey (intent.fulfillment.end.location)
  • When would they like to begin their journey (intent.fulfillment.start.time)
  • When would they like to end their journey (intent.fulfillment.end.time)
  • Who is the transport service provider they would like to avail services from (intent.provider)
  • Who is traveling (This is not recommended in public networks) (intent.fulfillment.customer)
  • What kind of fare product would they like to purchase (intent.item)
  • What add-on services would they like to avail
  • What offers would they like to apply on their booking (intent.offer)
  • What category of services would they like to avail (intent.category)
  • What additional luggage are they carrying
  • How would they like to pay for their journey (intent.payment)

For example, in health domain, a consumer declares the intent for a lab booking the describes various aspects of their booking like,
  • Where would they like to get their scan/test done (intent.fulfillment.start.location)
  • When would they like to get their scan/test done (intent.fulfillment.start.time)
  • When would they like to get the results of their test/scan (intent.fulfillment.end.time)
  • Who is the service provider they would like to avail services from (intent.provider)
  • Who is getting the test/scan (intent.fulfillment.customer)
  • What kind of test/scan would they like to purchase (intent.item)
  • What category of services would they like to avail (intent.category)
  • How would they like to pay for their journey (intent.payment)
' - type: object - properties: - descriptor: - description: 'A raw description of the search intent. Free text search strings, raw audio, etc can be sent in this object.' - allOf: - - $ref: '#/components/schemas/Descriptor' - provider: - description: The provider from which the customer wants to place to the order from - allOf: - - $ref: '#/components/schemas/Provider' - fulfillment: - description: Details on how the customer wants their order fulfilled - allOf: - - $ref: '#/components/schemas/Fulfillment' - payment: - description: Details on how the customer wants to pay for the order - allOf: - - $ref: '#/components/schemas/Payment' - category: - description: Details on the item category - allOf: - - $ref: '#/components/schemas/Category' - offer: - description: details on the offer the customer wants to avail - allOf: - - $ref: '#/components/schemas/Offer' - item: - description: Details of the item that the consumer wants to order - allOf: - - $ref: '#/components/schemas/Item' - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - ItemQuantity: - description: Describes the count or amount of an item - type: object - properties: - allocated: - description: This represents the exact quantity allocated for purchase of the item. - type: object - properties: - count: - type: integer - minimum: 0 - measure: - $ref: '#/components/schemas/Scalar' - available: - description: This represents the exact quantity available for purchase of the item. The buyer can only purchase multiples of this - type: object - properties: - count: - type: integer - minimum: 0 - measure: - $ref: '#/components/schemas/Scalar' - maximum: - description: This represents the maximum quantity allowed for purchase of the item - type: object - properties: - count: - type: integer - minimum: 1 - measure: - $ref: '#/components/schemas/Scalar' - minimum: - description: This represents the minimum quantity allowed for purchase of the item - type: object - properties: - count: - type: integer - minimum: 0 - measure: - $ref: '#/components/schemas/Scalar' - selected: - description: This represents the quantity selected for purchase of the item - type: object - properties: - count: - type: integer - minimum: 0 - measure: - $ref: '#/components/schemas/Scalar' - unitized: - description: This represents the quantity available in a single unit of the item - type: object - properties: - count: - type: integer - minimum: 1 - maximum: 1 - measure: - $ref: '#/components/schemas/Scalar' - Item: - description: 'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.' - type: object - properties: - id: - description: ID of the item. - type: string - parent_item_id: - description: 'ID of the item, this item is a variant of' - allOf: - - $ref: '#/components/schemas/Item/properties/id' - parent_item_quantity: - description: The number of units of the parent item this item is a multiple of - allOf: - - $ref: '#/components/schemas/ItemQuantity' - descriptor: - description: Physical description of the item - allOf: - - $ref: '#/components/schemas/Descriptor' - creator: - description: The creator of this item - allOf: - - $ref: '#/components/schemas/Organization' - price: - description: 'The price of this item, if it has intrinsic value' - allOf: - - $ref: '#/components/schemas/Price' - quantity: - description: The selling quantity of the item - allOf: - - $ref: '#/components/schemas/ItemQuantity' - category_ids: - description: Categories this item can be listed under - type: array - items: - allOf: - - $ref: '#/components/schemas/Category/properties/id' - fulfillment_ids: - description: Modes through which this item can be fulfilled - type: array - items: - allOf: - - $ref: '#/components/schemas/Fulfillment/properties/id' - location_ids: - description: Provider Locations this item is available in - type: array - items: - allOf: - - $ref: '#/components/schemas/Location/properties/id' - payment_ids: - description: Payment modalities through which this item can be ordered - type: array - items: - allOf: - - $ref: '#/components/schemas/Payment/properties/id' - add_ons: - type: array - items: - $ref: '#/components/schemas/AddOn' - cancellation_terms: - description: Cancellation terms of this item - type: array - items: - $ref: '#/components/schemas/CancellationTerm' - refund_terms: - description: Refund terms of this item - type: array - items: - description: Refund term of an item or an order - type: object - properties: - fulfillment_state: - description: The state of fulfillment during which this term is applicable. - allOf: - - $ref: '#/components/schemas/State' - refund_eligible: - description: Indicates if cancellation will result in a refund - type: boolean - refund_within: - description: Time within which refund will be processed after successful cancellation. - allOf: - - $ref: '#/components/schemas/Time' - refund_amount: - $ref: '#/components/schemas/Price' - replacement_terms: - description: Terms that are applicable be met when this item is replaced - type: array - items: - $ref: '#/components/schemas/ReplacementTerm' - return_terms: - description: Terms that are applicable when this item is returned - type: array - items: - $ref: '#/components/schemas/ReturnTerm' - xinput: - description: Additional input required from the customer to purchase / avail this item - allOf: - - $ref: '#/components/schemas/XInput' - time: - description: Temporal attributes of this item. This property is used when the item exists on the catalog only for a limited period of time. - allOf: - - $ref: '#/components/schemas/Time' - rateable: - description: Whether this item can be rated - type: boolean - rating: - description: The rating of the item - allOf: - - $ref: '#/components/schemas/Rating/properties/value' - matched: - description: Whether this item is an exact match of the request - type: boolean - related: - description: Whether this item is a related item to the exactly matched item - type: boolean - recommended: - description: Whether this item is a recommended item to a response - type: boolean - ttl: - description: Time to live in seconds for an instance of this schema - type: string - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Location: - description: The physical location of something - type: object - properties: - id: - type: string - descriptor: - $ref: '#/components/schemas/Descriptor' - map_url: - description: The url to the map of the location. This can be a globally recognized map url or the one specified by the network policy. - type: string - format: uri - gps: - description: The GPS co-ordinates of this location. - allOf: - - $ref: '#/components/schemas/Gps' - address: - description: The address of this location. - allOf: - - $ref: '#/components/schemas/Address' - city: - description: 'The city this location is, or is located within' - allOf: - - $ref: '#/components/schemas/City' - district: - description: 'The state this location is, or is located within' - type: string - state: - description: 'The state this location is, or is located within' - allOf: - - $ref: '#/components/schemas/State' - country: - description: 'The country this location is, or is located within' - allOf: - - $ref: '#/components/schemas/Country' - area_code: - type: string - circle: - $ref: '#/components/schemas/Circle' - polygon: - description: The boundary polygon of this location - type: string - 3dspace: - description: The three dimensional region describing this location - type: string - rating: - description: The rating of this location - allOf: - - $ref: '#/components/schemas/Rating/properties/value' - MediaFile: - description: This object contains a url to a media file. - type: object - properties: - mimetype: - description: 'indicates the nature and format of the document, file, or assortment of bytes. MIME types are defined and standardized in IETF''s RFC 6838' - type: string - url: - description: The URL of the file - type: string - format: uri - signature: - description: The digital signature of the file signed by the sender - type: string - dsa: - description: The signing algorithm used by the sender - type: string - Offer: - description: An offer associated with a catalog. This is typically used to promote a particular product and enable more purchases. - type: object - properties: - id: - type: string - descriptor: - $ref: '#/components/schemas/Descriptor' - location_ids: - type: array - items: - $ref: '#/components/schemas/Location/properties/id' - category_ids: - type: array - items: - $ref: '#/components/schemas/Category/properties/id' - item_ids: - type: array - items: - $ref: '#/components/schemas/Item/properties/id' - time: - $ref: '#/components/schemas/Time' - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Option: - description: Describes a selectable option - type: object - properties: - id: - type: string - descriptor: - $ref: '#/components/schemas/Descriptor' - Order: - description: Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller. - type: object - properties: - id: - type: string - description: Human-readable ID of the order. This is generated at the BPP layer. The BPP can either generate order id within its system or forward the order ID created at the provider level. - ref_order_ids: - description: A list of order IDs to link this order to previous orders. - type: array - items: - type: string - description: ID of a previous order - status: - description: Status of the order. Allowed values can be defined by the network policy - type: string - enum: - - ACTIVE - - COMPLETE - - CANCELLED - type: - description: 'This is used to indicate the type of order being created to BPPs. Sometimes orders can be linked to previous orders, like a replacement order in a retail domain. A follow-up consultation in healthcare domain. A single order part of a subscription order. The list of order types can be standardized at the network level.' - type: string - default: DEFAULT - enum: - - DRAFT - - DEFAULT - provider: - description: Details of the provider whose catalog items have been selected. - allOf: - - $ref: '#/components/schemas/Provider' - items: - description: The items purchased / availed in this order - type: array - items: - $ref: '#/components/schemas/Item' - add_ons: - description: The add-ons purchased / availed in this order - type: array - items: - $ref: '#/components/schemas/AddOn' - offers: - description: The offers applied in this order - type: array - items: - $ref: '#/components/schemas/Offer' - billing: - description: The billing details of this order - allOf: - - $ref: '#/components/schemas/Billing' - fulfillments: - description: The fulfillments involved in completing this order - type: array - items: - $ref: '#/components/schemas/Fulfillment' - cancellation: - description: The cancellation details of this order - allOf: - - $ref: '#/components/schemas/Cancellation' - cancellation_terms: - description: Cancellation terms of this item - type: array - items: - $ref: '#/components/schemas/CancellationTerm' - refund_terms: - description: Refund terms of this item - type: array - items: - $ref: '#/components/schemas/Item/properties/refund_terms/items' - replacement_terms: - description: Replacement terms of this item - type: array - items: - $ref: '#/components/schemas/ReplacementTerm' - return_terms: - description: Return terms of this item - type: array - items: - $ref: '#/components/schemas/ReturnTerm' - quote: - description: The mutually agreed upon quotation for this order. - allOf: - - $ref: '#/components/schemas/Quotation' - payments: - description: The terms of settlement for this order - type: array - items: - $ref: '#/components/schemas/Payment' - created_at: - description: The date-time of creation of this order - type: string - format: date-time - updated_at: - description: The date-time of updated of this order - type: string - format: date-time - xinput: - description: Additional input required from the customer to confirm this order - allOf: - - $ref: '#/components/schemas/XInput' - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Organization: - description: An organization. Usually a recognized business entity. - type: object - properties: - descriptor: - $ref: '#/components/schemas/Descriptor' - address: - description: The postal address of the organization - allOf: - - $ref: '#/components/schemas/Address' - state: - description: The state where the organization's address is registered - allOf: - - $ref: '#/components/schemas/State' - city: - description: The city where the the organization's address is registered - allOf: - - $ref: '#/components/schemas/City' - contact: - $ref: '#/components/schemas/Contact' - Payment: - description: 'Describes the terms of settlement between the BAP and the BPP for a single transaction. When instantiated, this object contains
  1. the amount that has to be settled,
  2. The payment destination destination details
  3. When the settlement should happen, and
  4. A transaction reference ID
. During a transaction, the BPP reserves the right to decide the terms of payment. However, the BAP can send its terms to the BPP first. If the BPP does not agree to those terms, it must overwrite the terms and return them to the BAP. If overridden, the BAP must either agree to the terms sent by the BPP in order to preserve the provider''s autonomy, or abort the transaction. In case of such disagreements, the BAP and the BPP can perform offline negotiations on the payment terms. Once an agreement is reached, the BAP and BPP can resume transactions.' - type: object - properties: - id: - description: ID of the payment term that can be referred at an item or an order level in a catalog - type: string - collected_by: - description: 'This field indicates who is the collector of payment. The BAP can set this value to ''bap'' if it wants to collect the payment first and settle it to the BPP. If the BPP agrees to those terms, the BPP should not send the payment url. Alternatively, the BPP can set this field with the value ''bpp'' if it wants the payment to be made directly.' - url: - type: string - description: 'A payment url to be called by the BAP. If empty, then the payment is to be done offline. The details of payment should be present in the params object. If tl_method = http/get, then the payment details will be sent as url params. Two url param values, ```$transaction_id``` and ```$amount``` are mandatory.' - format: uri - params: - type: object - properties: - transaction_id: - type: string - description: The reference transaction ID associated with a payment activity - amount: - type: string - currency: - type: string - bank_code: - type: string - bank_account_number: - type: string - virtual_payment_address: - type: string - source_bank_code: - type: string - source_bank_account_number: - type: string - source_virtual_payment_address: - type: string - type: - type: string - enum: - - PRE-ORDER - - PRE-FULFILLMENT - - ON-FULFILLMENT - - POST-FULFILLMENT - status: - type: string - enum: - - PAID - - NOT-PAID - time: - $ref: '#/components/schemas/Time' - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Person: - description: Describes a person as any individual - type: object - properties: - id: - type: string - description: Describes the identity of the person - url: - description: Profile url of the person - type: string - format: uri - name: - description: the name of the person - type: string - image: - $ref: '#/components/schemas/Image' - age: - description: Age of the person - allOf: - - $ref: '#/components/schemas/Duration' - dob: - description: Date of birth of the person - type: string - format: date - gender: - type: string - description: 'Gender of something, typically a Person, but possibly also fictional characters, animals, etc. While Male and Female may be used, text strings are also acceptable for people who do not identify as a binary gender.Allowed values for this field can be published in the network policy' - creds: - type: array - items: - $ref: '#/components/schemas/Credential' - languages: - type: array - items: - description: Describes a language known to the person. - type: object - properties: - code: - type: string - name: - type: string - skills: - type: array - items: - description: Describes a skill of the person. - type: object - properties: - code: - type: string - name: - type: string - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Price: - description: Describes the price of a product or service - type: object - properties: - currency: - type: string - value: - $ref: '#/components/schemas/DecimalValue' - estimated_value: - $ref: '#/components/schemas/DecimalValue' - computed_value: - $ref: '#/components/schemas/DecimalValue' - listed_value: - $ref: '#/components/schemas/DecimalValue' - offered_value: - $ref: '#/components/schemas/DecimalValue' - minimum_value: - $ref: '#/components/schemas/DecimalValue' - maximum_value: - $ref: '#/components/schemas/DecimalValue' - Provider: - description: Describes the catalog of a business. - type: object - properties: - id: - type: string - description: Id of the provider - descriptor: - $ref: '#/components/schemas/Descriptor' - category_id: - type: string - description: Category Id of the provider at the BPP-level catalog - rating: - $ref: '#/components/schemas/Rating/properties/value' - time: - $ref: '#/components/schemas/Time' - categories: - type: array - items: - $ref: '#/components/schemas/Category' - fulfillments: - type: array - items: - $ref: '#/components/schemas/Fulfillment' - payments: - type: array - items: - $ref: '#/components/schemas/Payment' - locations: - type: array - items: - $ref: '#/components/schemas/Location' - offers: - type: array - items: - $ref: '#/components/schemas/Offer' - items: - type: array - items: - $ref: '#/components/schemas/Item' - exp: - type: string - description: Time after which catalog has to be refreshed - format: date-time - rateable: - description: Whether this provider can be rated or not - type: boolean - ttl: - description: 'The time-to-live in seconds, for this object. This can be overriden at deeper levels. A value of -1 indicates that this object is not cacheable.' - type: integer - minimum: -1 - tags: - type: array - items: - $ref: '#/components/schemas/TagGroup' - Quotation: - description: 'Describes a quote. It is the estimated price of products or services from the BPP.
This has properties like price, breakup, ttl' - type: object - properties: - id: - description: ID of the quote. - type: string - format: uuid - price: - description: The total quoted price - allOf: - - $ref: '#/components/schemas/Price' - breakup: - description: the breakup of the total quoted price - type: array - items: - type: object - properties: - item: - $ref: '#/components/schemas/Item' - title: - type: string - price: - $ref: '#/components/schemas/Price' - ttl: - $ref: '#/components/schemas/Duration' - Rating: - description: Describes the rating of an entity - type: object - properties: - rating_category: - description: Category of the entity being rated - type: string - enum: - - Item - - Order - - Fulfillment - - Provider - - Agent - - Support - id: - description: Id of the object being rated - type: string - value: - description: 'Rating value given to the object. This can be a single value or can also contain an inequality operator like gt, gte, lt, lte. This can also contain an inequality expression containing logical operators like && and ||.' - type: string - Region: - description: Describes an arbitrary region of space. The network policy should contain a published list of supported regions by the network. - type: object - properties: - dimensions: - description: 'The number of dimensions that are used to describe any point inside that region. The most common dimensionality of a region is 2, that represents an area on a map. There are regions on the map that can be approximated to one-dimensional regions like roads, railway lines, or shipping lines. 3 dimensional regions are rarer, but are gaining popularity as flying drones are being adopted for various fulfillment services.' - type: string - enum: - - '1' - - '2' - - '3' - type: - description: 'The type of region. This is used to specify the granularity of the region represented by this object. Various examples of two-dimensional region types are city, country, state, district, and so on. The network policy should contain a list of all possible region types supported by the network.' - type: string - name: - type: string - description: Name of the region as specified on the map where that region exists. - code: - type: string - description: A standard code representing the region. This should be interpreted in the same way by all network participants. - boundary: - type: string - description: 'A string representing the boundary of the region. One-dimensional regions are represented by polylines. Two-dimensional regions are represented by polygons, and three-dimensional regions can represented by polyhedra.' - map_url: - type: string - description: The url to the map of the region. This can be a globally recognized map or the one specified by the network policy. - ReplacementTerm: - description: The replacement policy of an item or an order - type: object - properties: - fulfillment_state: - description: The state of fulfillment during which this term is applicable. - allOf: - - $ref: '#/components/schemas/State' - replace_within: - description: 'Applicable only for buyer managed returns where the buyer has to replace the item before a certain date-time, failing which they will not be eligible for replacement' - allOf: - - $ref: '#/components/schemas/Time' - external_ref: - $ref: '#/components/schemas/MediaFile' - ReturnTerm: - description: Describes the return policy of an item or an order - type: object - properties: - fulfillment_state: - description: The state of fulfillment during which this term IETF''s applicable. - allOf: - - $ref: '#/components/schemas/State' - return_eligible: - description: Indicates whether the item is eligible for return - type: boolean - return_time: - description: 'Applicable only for buyer managed returns where the buyer has to return the item to the origin before a certain date-time, failing which they will not be eligible for refund.' - allOf: - - $ref: '#/components/schemas/Time' - return_location: - description: The location where the item or order must / will be returned to - allOf: - - $ref: '#/components/schemas/Location' - fulfillment_managed_by: - description: The entity that will perform the return - type: string - enum: - - CONSUMER - - PROVIDER - Scalar: - description: Describes a scalar - type: object - properties: - type: - type: string - enum: - - CONSTANT - - VARIABLE - value: - $ref: '#/components/schemas/DecimalValue' - estimated_value: - $ref: '#/components/schemas/DecimalValue' - computed_value: - $ref: '#/components/schemas/DecimalValue' - range: - type: object - properties: - min: - $ref: '#/components/schemas/DecimalValue' - max: - $ref: '#/components/schemas/DecimalValue' - unit: - type: string - Schedule: - description: 'Describes schedule as a repeating time period used to describe a regularly recurring event. At a minimum a schedule will specify frequency which describes the interval between occurrences of the event. Additional information can be provided to specify the schedule more precisely. This includes identifying the timestamps(s) of when the event will take place. Schedules may also have holidays to exclude a specific day from the schedule.
This has properties like frequency, holidays, times' - type: object - properties: - frequency: - $ref: '#/components/schemas/Duration' - holidays: - type: array - items: - type: string - format: date-time - times: - type: array - items: - type: string - format: date-time - State: - description: A bounded geopolitical region of governance inside a country. - type: object - properties: - name: - type: string - description: Name of the state - code: - type: string - description: State code as per country or international standards - Stop: - description: A logical point in space and time during the fulfillment of an order. - type: object - properties: - id: - type: string - parent_stop_id: - type: string - location: - description: Location of the stop - allOf: - - $ref: '#/components/schemas/Location' - type: - description: The type of stop. Allowed values of this property can be defined by the network policy. - type: string - time: - description: Timings applicable at the stop. - allOf: - - $ref: '#/components/schemas/Time' - instructions: - description: Instructions that need to be followed at the stop - allOf: - - $ref: '#/components/schemas/Descriptor' - contact: - description: Contact details of the stop - allOf: - - $ref: '#/components/schemas/Contact' - person: - description: The details of the person present at the stop - allOf: - - $ref: '#/components/schemas/Person' - authorization: - $ref: '#/components/schemas/Authorization' - Support: - description: Details of customer support - type: object - properties: - ref_id: - type: string - callback_phone: - type: string - format: phone - phone: - type: string - format: phone - email: - type: string - format: email - url: - type: string - format: uri - Tag: - description: 'Describes a tag. This is used to contain extended metadata. This object can be added as a property to any schema to describe extended attributes. For BAPs, tags can be sent during search to optimize and filter search results. BPPs can use tags to index their catalog to allow better search functionality. Tags are sent by the BPP as part of the catalog response in the `on_search` callback. Tags are also meant for display purposes. Upon receiving a tag, BAPs are meant to render them as name-value pairs. This is particularly useful when rendering tabular information about a product or service.' - type: object - properties: - descriptor: - description: 'Description of the Tag, can be used to store detailed information.' - allOf: - - $ref: '#/components/schemas/Descriptor' - value: - description: The value of the tag. This set by the BPP and rendered as-is by the BAP. - type: string - display: - description: 'This value indicates if the tag is intended for display purposes. If set to `true`, then this tag must be displayed. If it is set to `false`, it should not be displayed. This value can override the group display value.' - type: boolean - TagGroup: - description: 'A collection of tag objects with group level attributes. For detailed documentation on the Tags and Tag Groups schema go to https://github.com/beckn/protocol-specifications/discussions/316' - type: object - properties: - display: - description: 'Indicates the display properties of the tag group. If display is set to false, then the group will not be displayed. If it is set to true, it should be displayed. However, group-level display properties can be overriden by individual tag-level display property. As this schema is purely for catalog display purposes, it is not recommended to send this value during search.' - type: boolean - default: true - descriptor: - description: 'Description of the TagGroup, can be used to store detailed information.' - allOf: - - $ref: '#/components/schemas/Descriptor' - list: - description: 'An array of Tag objects listed under this group. This property can be set by BAPs during search to narrow the `search` and achieve more relevant results. When received during `on_search`, BAPs must render this list under the heading described by the `name` property of this schema.' - type: array - items: - $ref: '#/components/schemas/Tag' - Time: - description: 'Describes time in its various forms. It can be a single point in time; duration; or a structured timetable of operations
This has properties like label, time stamp,duration,range, days, schedule' - type: object - properties: - label: - type: string - timestamp: - type: string - format: date-time - duration: - $ref: '#/components/schemas/Duration' - range: - type: object - properties: - start: - type: string - format: date-time - end: - type: string - format: date-time - days: - type: string - description: comma separated values representing days of the week - schedule: - $ref: '#/components/schemas/Schedule' - Tracking: - description: Contains tracking information that can be used by the BAP to track the fulfillment of an order in real-time. which is useful for knowing the location of time sensitive deliveries. - type: object - properties: - id: - description: A unique tracking reference number - type: string - url: - description: 'A URL to the tracking endpoint. This can be a link to a tracking webpage, a webhook URL created by the BAP where BPP can push the tracking data, or a GET url creaed by the BPP which the BAP can poll to get the tracking data. It can also be a websocket URL where the BPP can push real-time tracking data.' - type: string - format: uri - location: - description: 'In case there is no real-time tracking endpoint available, this field will contain the latest location of the entity being tracked. The BPP will update this value everytime the BAP calls the track API.' - allOf: - - $ref: '#/components/schemas/Location' - status: - description: 'This value indicates if the tracking is currently active or not. If this value is `active`, then the BAP can begin tracking the order. If this value is `inactive`, the tracking URL is considered to be expired and the BAP should stop tracking the order.' - type: string - enum: - - active - - inactive - Vehicle: - description: 'Describes a vehicle is a device that is designed or used to transport people or cargo over land, water, air, or through space.
This has properties like category, capacity, make, model, size,variant,color,energy_type,registration' - type: object - properties: - category: - type: string - capacity: - type: integer - make: - type: string - model: - type: string - size: - type: string - variant: - type: string - color: - type: string - energy_type: - type: string - registration: - type: string - wheels_count: - type: string - cargo_volumne: - type: string - wheelchair_access: - type: string - code: - type: string - emission_standard: - type: string - XInput: - description: 'Contains any additional or extended inputs required to confirm an order. This is typically a Form Input. Sometimes, selection of catalog elements is not enough for the BPP to confirm an order. For example, to confirm a flight ticket, the airline requires details of the passengers along with information on baggage, identity, in addition to the class of ticket. Similarly, a logistics company may require details on the nature of shipment in order to confirm the shipping. A recruiting firm may require additional details on the applicant in order to confirm a job application. For all such purposes, the BPP can choose to send this object attached to any object in the catalog that is required to be sent while placing the order. This object can typically be sent at an item level or at the order level. The item level XInput will override the Order level XInput as it indicates a special requirement of information for that particular item. Hence the BAP must render a separate form for the Item and another form at the Order level before confirmation.' - type: object - properties: - form: - $ref: '#/components/schemas/Form' - required: - description: Indicates whether the form data is mandatorily required by the BPP to confirm the order. - type: boolean \ No newline at end of file diff --git a/schemas/core_1.1.0/confirm.yml b/schemas/core_1.1.0/confirm.yml deleted file mode 100644 index 638cee9..0000000 --- a/schemas/core_1.1.0/confirm.yml +++ /dev/null @@ -1,236 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn Protocol Core - description: Beckn Core Transaction API specification - version: 1.1.0 -security: - - SubscriberAuth: [] -paths: - /confirm: - post: - tags: - - Beckn Provider Platform (BPP) - description: Initialize an order by providing billing and/or shipping details - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - confirm - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message -components: - schemas: - Context: - required: - - action - - version - - bap_id - - bap_uri - - domain - - message_id - - transaction_id - - bpp_id - - bpp_uri - description: 'Every API call in beckn protocol has a context. It provides a high-level overview to the receiver about the nature of the intended transaction. Typically, it is the BAP that sets the transaction context based on the consumer''s location and action on their UI. But sometimes, during unsolicited callbacks, the BPP also sets the transaction context but it is usually the same as the context of a previous full-cycle, request-callback interaction between the BAP and the BPP. The context object contains four types of fields.
  1. Demographic information about the transaction using fields like `domain`, `country`, and `region`.
  2. Addressing details like the sending and receiving platform''s ID and API URL.
  3. Interoperability information like the protocol version that implemented by the sender and,
  4. Transaction details like the method being called at the receiver''s endpoint, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specifiy the validity of the request, and a key to encrypt information if necessary.
This object must be passed in every interaction between a BAP and a BPP. In HTTP/S implementations, it is not necessary to send the context during the synchronous response. However, in asynchronous protocols, the context must be sent during all interactions,' - type: object - properties: - domain: - description: Domain code that is relevant to this transaction context - location: - description: The location where the transaction is intended to be fulfilled. - action: - description: The Beckn protocol method being called by the sender and executed at the receiver. - type: string - version: - type: string - description: Version of transaction protocol being used by the sender. - bap_id: - description: Subscriber ID of the BAP - allOf: - - description: 'A globally unique identifier of the platform, Typically it is the fully qualified domain name (FQDN) of the platform.' - type: string - bap_uri: - description: Subscriber URL of the BAP for accepting callbacks from BPPs. - allOf: - - description: The callback URL of the Subscriber. This should necessarily contain the same domain name as set in `subscriber_id``. - type: string - format: uri - bpp_id: - description: Subscriber ID of the BPP - allOf: - - $ref: '#/components/schemas/Context/properties/bap_id/allOf/0' - bpp_uri: - description: Subscriber URL of the BPP for accepting calls from BAPs. - allOf: - - $ref: '#/components/schemas/Context/properties/bap_uri/allOf/0' - transaction_id: - description: 'This is a unique value which persists across all API calls from `search` through `confirm`. This is done to indicate an active user session across multiple requests. The BPPs can use this value to push personalized recommendations, and dynamic offerings related to an ongoing transaction despite being unaware of the user active on the BAP.' - type: string - format: uuid - message_id: - description: 'This is a unique value which persists during a request / callback cycle. Since beckn protocol APIs are asynchronous, BAPs need a common value to match an incoming callback from a BPP to an earlier call. This value can also be used to ignore duplicate messages coming from the BPP. It is recommended to generate a fresh message_id for every new interaction. When sending unsolicited callbacks, BPPs must generate a new message_id.' - type: string - format: uuid - timestamp: - description: Time of request generation in RFC3339 format - type: string - format: date-time - key: - description: The encryption public key of the sender - type: string - ttl: - description: The duration in ISO8601 format after timestamp for which this message holds valid - type: string - Order: - description: Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller. - type: object - required: - - items - - billing - - fulfillments - properties: - items: - description: The items purchased / availed in this order. This should be based on what was used in the `select` call. - type: array - items: - $ref: '#/components/schemas/Item' - billing: - description: The billing details of this order - allOf: - - $ref: '#/components/schemas/Billing' - fulfillments: - description: The fulfillments involved in completing this order. - type: array - items: - $ref: '#/components/schemas/Fulfillment' - Descriptor: - description: Physical description of something. - type: object - properties: - name: - type: string - code: - type: string - FulfillmentState: - description: Describes the state of fulfillment - type: object - properties: - descriptor: - $ref: '#/components/schemas/Descriptor' - updated_at: - type: string - format: date-time - updated_by: - type: string - description: ID of entity which changed the state - Fulfillment: - description: Describes how a an order will be rendered/fulfilled to the end-customer. - type: object - required: - - id - - customer - properties: - id: - description: Unique reference ID to the fulfillment of an order - type: string - type: - description: 'A code that describes the mode of fulfillment. This is typically set when there are multiple ways an order can be fulfilled. For example, a retail order can be fulfilled either via store pickup or a home delivery. Similarly, a medical consultation can be provided either in-person or via tele-consultation. The network policy must publish standard fulfillment type codes for the different modes of fulfillment.' - type: string - rateable: - description: Whether the fulfillment can be rated or not - type: boolean - state: - description: The current state of fulfillment. The BPP must set this value whenever the state of the order fulfillment changes and fire an unsolicited `on_status` call. - allOf: - - $ref: '#/components/schemas/FulfillmentState' - tracking: - type: boolean - description: Indicates whether the fulfillment allows tracking - default: false - customer: - description: The person that will ultimately receive the order - allOf: - - $ref: '#/components/schemas/Customer' - path: - description: The physical path taken by the agent that can be rendered on a map. The allowed format of this property can be set by the network. - type: string - Person: - description: Describes a person as any individual - type: object - properties: - name: - description: the name of the person - type: string - Customer: - description: Describes a customer buying/availing a product or a service. These should be picker up from the customer profile. - type: object - required: - - person - - contact - properties: - person: - $ref: '#/components/schemas/Person' - contact: - $ref: '#/components/schemas/Contact' - Contact: - description: Describes the contact information of an entity - type: object - properties: - phone: - type: string - email: - type: string - Billing: - description: 'Describes the billing details of an entity.
This has properties like name,organization,address,email,phone,time,tax_number, created_at,updated_at' - type: object - properties: - name: - description: Name of the billable entity - type: string - email: - description: Email address where the bill is sent to - type: string - format: email - phone: - description: Phone number of the billable entity - type: string - ItemQuantity: - description: Describes the count or amount of an item - type: object - properties: - selected: - description: This represents the quantity selected for purchase of the item - type: object - properties: - count: - type: integer - minimum: 0 - Item: - description: 'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.' - type: object - properties: - id: - description: ID of the item. - type: string - quantity: - description: The selling quantity of the item - allOf: - - $ref: '#/components/schemas/ItemQuantity' \ No newline at end of file diff --git a/schemas/core_1.1.0/init.yml b/schemas/core_1.1.0/init.yml deleted file mode 100644 index 025c6d3..0000000 --- a/schemas/core_1.1.0/init.yml +++ /dev/null @@ -1,153 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn Protocol Core - description: Beckn Core Transaction API specification - version: 1.1.0 -security: - - SubscriberAuth: [] -paths: - /init: - post: - tags: - - Beckn Provider Platform (BPP) - description: Initialize an order by providing billing and/or shipping details - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - init - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message -components: - schemas: - Context: - required: - - action - - version - - bap_id - - bap_uri - - domain - - message_id - - transaction_id - - bpp_id - - bpp_uri - description: 'Every API call in beckn protocol has a context. It provides a high-level overview to the receiver about the nature of the intended transaction. Typically, it is the BAP that sets the transaction context based on the consumer''s location and action on their UI. But sometimes, during unsolicited callbacks, the BPP also sets the transaction context but it is usually the same as the context of a previous full-cycle, request-callback interaction between the BAP and the BPP. The context object contains four types of fields.
  1. Demographic information about the transaction using fields like `domain`, `country`, and `region`.
  2. Addressing details like the sending and receiving platform''s ID and API URL.
  3. Interoperability information like the protocol version that implemented by the sender and,
  4. Transaction details like the method being called at the receiver''s endpoint, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specifiy the validity of the request, and a key to encrypt information if necessary.
This object must be passed in every interaction between a BAP and a BPP. In HTTP/S implementations, it is not necessary to send the context during the synchronous response. However, in asynchronous protocols, the context must be sent during all interactions,' - type: object - properties: - domain: - description: Domain code that is relevant to this transaction context - location: - description: The location where the transaction is intended to be fulfilled. - action: - description: The Beckn protocol method being called by the sender and executed at the receiver. - type: string - version: - type: string - description: Version of transaction protocol being used by the sender. - bap_id: - description: Subscriber ID of the BAP - allOf: - - description: 'A globally unique identifier of the platform, Typically it is the fully qualified domain name (FQDN) of the platform.' - type: string - bap_uri: - description: Subscriber URL of the BAP for accepting callbacks from BPPs. - allOf: - - description: The callback URL of the Subscriber. This should necessarily contain the same domain name as set in `subscriber_id``. - type: string - format: uri - bpp_id: - description: Subscriber ID of the BPP - allOf: - - $ref: '#/components/schemas/Context/properties/bap_id/allOf/0' - bpp_uri: - description: Subscriber URL of the BPP for accepting calls from BAPs. - allOf: - - $ref: '#/components/schemas/Context/properties/bap_uri/allOf/0' - transaction_id: - description: 'This is a unique value which persists across all API calls from `search` through `confirm`. This is done to indicate an active user session across multiple requests. The BPPs can use this value to push personalized recommendations, and dynamic offerings related to an ongoing transaction despite being unaware of the user active on the BAP.' - type: string - format: uuid - message_id: - description: 'This is a unique value which persists during a request / callback cycle. Since beckn protocol APIs are asynchronous, BAPs need a common value to match an incoming callback from a BPP to an earlier call. This value can also be used to ignore duplicate messages coming from the BPP. It is recommended to generate a fresh message_id for every new interaction. When sending unsolicited callbacks, BPPs must generate a new message_id.' - type: string - format: uuid - timestamp: - description: Time of request generation in RFC3339 format - type: string - format: date-time - key: - description: The encryption public key of the sender - type: string - ttl: - description: The duration in ISO8601 format after timestamp for which this message holds valid - type: string - Order: - description: Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller. - type: object - required: - - items - - billing - properties: - billing: - description: The billing details of this order - allOf: - - $ref: '#/components/schemas/Billing' - items: - description: The items purchased / availed in this order - type: array - items: - $ref: '#/components/schemas/Item' - Billing: - description: 'Describes the billing details of an entity.
This has properties like name,organization,address,email,phone,time,tax_number, created_at,updated_at' - type: object - properties: - name: - description: Name of the billable entity - type: string - email: - description: Email address where the bill is sent to - type: string - format: email - phone: - description: Phone number of the billable entity - type: string - ItemQuantity: - description: Describes the count or amount of an item - type: object - properties: - selected: - description: This represents the quantity selected for purchase of the item - type: object - properties: - count: - type: integer - minimum: 0 - Item: - description: 'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.' - type: object - properties: - id: - description: ID of the item. - type: string - quantity: - description: The selling quantity of the item - allOf: - - $ref: '#/components/schemas/ItemQuantity' \ No newline at end of file diff --git a/schemas/core_1.1.0/search.yml b/schemas/core_1.1.0/search.yml deleted file mode 100644 index 9d52ae4..0000000 --- a/schemas/core_1.1.0/search.yml +++ /dev/null @@ -1,122 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn Protocol Core - description: Beckn Core Transaction API specification - version: 1.1.0 -security: - - SubscriberAuth: [] -paths: - /search: - post: - tags: - - Beckn Provider Platform (BPP) - - Beckn Gateway (BG) - description: BAP declares the customer's intent to buy/avail products or services - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - search - message: - type: object - properties: - intent: - $ref: '#/components/schemas/Intent' - required: - - context - - message -components: - schemas: - Context: - required: - - action - - version - - bap_id - - bap_uri - - domain - - message_id - description: 'Every API call in beckn protocol has a context. It provides a high-level overview to the receiver about the nature of the intended transaction. Typically, it is the BAP that sets the transaction context based on the consumer''s location and action on their UI. But sometimes, during unsolicited callbacks, the BPP also sets the transaction context but it is usually the same as the context of a previous full-cycle, request-callback interaction between the BAP and the BPP. The context object contains four types of fields.
  1. Demographic information about the transaction using fields like `domain`, `country`, and `region`.
  2. Addressing details like the sending and receiving platform''s ID and API URL.
  3. Interoperability information like the protocol version that implemented by the sender and,
  4. Transaction details like the method being called at the receiver''s endpoint, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specifiy the validity of the request, and a key to encrypt information if necessary.
This object must be passed in every interaction between a BAP and a BPP. In HTTP/S implementations, it is not necessary to send the context during the synchronous response. However, in asynchronous protocols, the context must be sent during all interactions,' - type: object - properties: - domain: - description: Domain code that is relevant to this transaction context - location: - description: The location where the transaction is intended to be fulfilled. - action: - description: The Beckn protocol method being called by the sender and executed at the receiver. - type: string - version: - type: string - description: Version of transaction protocol being used by the sender. - bap_id: - description: Subscriber ID of the BAP - allOf: - - description: 'A globally unique identifier of the platform, Typically it is the fully qualified domain name (FQDN) of the platform.' - type: string - bap_uri: - description: Subscriber URL of the BAP for accepting callbacks from BPPs. - allOf: - - description: The callback URL of the Subscriber. This should necessarily contain the same domain name as set in `subscriber_id``. - type: string - format: uri - transaction_id: - description: 'This is a unique value which persists across all API calls from `search` through `confirm`. This is done to indicate an active user session across multiple requests. The BPPs can use this value to push personalized recommendations, and dynamic offerings related to an ongoing transaction despite being unaware of the user active on the BAP.' - type: string - format: uuid - message_id: - description: 'This is a unique value which persists during a request / callback cycle. Since beckn protocol APIs are asynchronous, BAPs need a common value to match an incoming callback from a BPP to an earlier call. This value can also be used to ignore duplicate messages coming from the BPP. It is recommended to generate a fresh message_id for every new interaction. When sending unsolicited callbacks, BPPs must generate a new message_id.' - type: string - format: uuid - timestamp: - description: Time of request generation in RFC3339 format - type: string - format: date-time - key: - description: The encryption public key of the sender - type: string - ttl: - description: The duration in ISO8601 format after timestamp for which this message holds valid - type: string - Intent: - description: 'The intent to buy or avail a product or a service. The BAP can declare the intent of the consumer containing
  • What they want (A product, service, offer)
  • Who they want (A seller, service provider, agent etc)
  • Where they want it and where they want it from
  • When they want it (start and end time of fulfillment
  • How they want to pay for it

This has properties like descriptor,provider,fulfillment,payment,category,offer,item,tags
This is typically used by the BAP to send the purpose of the user''s search to the BPP. This will be used by the BPP to find products or services it offers that may match the user''s intent.
For example, in Mobility, the mobility consumer declares a mobility intent. In this case, the mobility consumer declares information that describes various aspects of their journey like,
  • Where would they like to begin their journey (intent.fulfillment.start.location)
  • Where would they like to end their journey (intent.fulfillment.end.location)
  • When would they like to begin their journey (intent.fulfillment.start.time)
  • When would they like to end their journey (intent.fulfillment.end.time)
  • Who is the transport service provider they would like to avail services from (intent.provider)
  • Who is traveling (This is not recommended in public networks) (intent.fulfillment.customer)
  • What kind of fare product would they like to purchase (intent.item)
  • What add-on services would they like to avail
  • What offers would they like to apply on their booking (intent.offer)
  • What category of services would they like to avail (intent.category)
  • What additional luggage are they carrying
  • How would they like to pay for their journey (intent.payment)

For example, in health domain, a consumer declares the intent for a lab booking the describes various aspects of their booking like,
  • Where would they like to get their scan/test done (intent.fulfillment.start.location)
  • When would they like to get their scan/test done (intent.fulfillment.start.time)
  • When would they like to get the results of their test/scan (intent.fulfillment.end.time)
  • Who is the service provider they would like to avail services from (intent.provider)
  • Who is getting the test/scan (intent.fulfillment.customer)
  • What kind of test/scan would they like to purchase (intent.item)
  • What category of services would they like to avail (intent.category)
  • How would they like to pay for their journey (intent.payment)
' - type: object - properties: - descriptor: - description: 'A raw description of the search intent. Free text search strings, raw audio, etc can be sent in this object.' - allOf: - - $ref: '#/components/schemas/Descriptor' - Descriptor: - description: Physical description of something. - type: object - required: - - name - properties: - name: - type: string - description: It should contain the search query string such as product or service name. - code: - type: string - short_desc: - type: string - long_desc: - type: string - additional_desc: - type: object - properties: - url: - type: string - content_type: - type: string - enum: - - text/plain - - text/html - - application/json - \ No newline at end of file diff --git a/schemas/core_1.1.0/select.yml b/schemas/core_1.1.0/select.yml deleted file mode 100644 index 89f86e0..0000000 --- a/schemas/core_1.1.0/select.yml +++ /dev/null @@ -1,132 +0,0 @@ -openapi: 3.0.0 -info: - title: Beckn Protocol Core - description: Beckn Core Transaction API specification - version: 1.1.0 -security: - - SubscriberAuth: [] -paths: - /select: - post: - tags: - - Beckn Provider Platform (BPP) - description: BAP declares the customer's cart (or equivalent) created by selecting objects from the catalog - requestBody: - content: - application/json: - schema: - type: object - properties: - context: - allOf: - - $ref: '#/components/schemas/Context' - - properties: - action: - enum: - - select - required: - - action - message: - type: object - properties: - order: - $ref: '#/components/schemas/Order' - required: - - order - required: - - context - - message -components: - schemas: - Context: - required: - - action - - version - - bap_id - - bap_uri - - domain - - message_id - - transaction_id - - bpp_id - - bpp_uri - description: 'Every API call in beckn protocol has a context. It provides a high-level overview to the receiver about the nature of the intended transaction. Typically, it is the BAP that sets the transaction context based on the consumer''s location and action on their UI. But sometimes, during unsolicited callbacks, the BPP also sets the transaction context but it is usually the same as the context of a previous full-cycle, request-callback interaction between the BAP and the BPP. The context object contains four types of fields.
  1. Demographic information about the transaction using fields like `domain`, `country`, and `region`.
  2. Addressing details like the sending and receiving platform''s ID and API URL.
  3. Interoperability information like the protocol version that implemented by the sender and,
  4. Transaction details like the method being called at the receiver''s endpoint, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specifiy the validity of the request, and a key to encrypt information if necessary.
This object must be passed in every interaction between a BAP and a BPP. In HTTP/S implementations, it is not necessary to send the context during the synchronous response. However, in asynchronous protocols, the context must be sent during all interactions,' - type: object - properties: - domain: - description: Domain code that is relevant to this transaction context - location: - description: The location where the transaction is intended to be fulfilled. - action: - description: The Beckn protocol method being called by the sender and executed at the receiver. - type: string - version: - type: string - description: Version of transaction protocol being used by the sender. - bap_id: - description: Subscriber ID of the BAP - allOf: - - description: 'A globally unique identifier of the platform, Typically it is the fully qualified domain name (FQDN) of the platform.' - type: string - bap_uri: - description: Subscriber URL of the BAP for accepting callbacks from BPPs. - allOf: - - description: The callback URL of the Subscriber. This should necessarily contain the same domain name as set in `subscriber_id``. - type: string - format: uri - bpp_id: - description: Subscriber ID of the BPP - allOf: - - $ref: '#/components/schemas/Context/properties/bap_id/allOf/0' - bpp_uri: - description: Subscriber URL of the BPP for accepting calls from BAPs. - allOf: - - $ref: '#/components/schemas/Context/properties/bap_uri/allOf/0' - transaction_id: - description: 'This is a unique value which persists across all API calls from `search` through `confirm`. This is done to indicate an active user session across multiple requests. The BPPs can use this value to push personalized recommendations, and dynamic offerings related to an ongoing transaction despite being unaware of the user active on the BAP.' - type: string - format: uuid - message_id: - description: 'This is a unique value which persists during a request / callback cycle. Since beckn protocol APIs are asynchronous, BAPs need a common value to match an incoming callback from a BPP to an earlier call. This value can also be used to ignore duplicate messages coming from the BPP. It is recommended to generate a fresh message_id for every new interaction. When sending unsolicited callbacks, BPPs must generate a new message_id.' - type: string - format: uuid - timestamp: - description: Time of request generation in RFC3339 format - type: string - format: date-time - key: - description: The encryption public key of the sender - type: string - ttl: - description: The duration in ISO8601 format after timestamp for which this message holds valid - type: string - Order: - description: Describes a legal purchase order. It contains the complete details of the legal contract created between the buyer and the seller. - type: object - properties: - items: - description: The items purchased / availed in this order - type: array - items: - $ref: '#/components/schemas/Item' - ItemQuantity: - description: Describes the count or amount of an item - type: object - properties: - selected: - description: This represents the quantity selected for purchase of the item - type: object - properties: - count: - type: integer - minimum: 0 - Item: - description: 'Describes a product or a service offered to the end consumer by the provider. In the mobility sector, it can represent a fare product like one way journey. In the logistics sector, it can represent the delivery service offering. In the retail domain it can represent a product like a grocery item.' - type: object - properties: - id: - description: ID of the item. In case of a select, this should be the id of item selected. - type: string - quantity: - description: The selling quantity of the item. In case of a select, this should be the quantity selected by the user. - allOf: - - $ref: '#/components/schemas/ItemQuantity' \ No newline at end of file diff --git a/schemas/jsons/actions.js b/schemas/jsons/actions.js deleted file mode 100644 index 7eb6c53..0000000 --- a/schemas/jsons/actions.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - type: "object", - properties: { - action:{ - type:"string", - description: "action that the user wants to perform. This should be one of th actions defined by supported actions. If its not one of the actions, its value should be null." - }, - transaction_id:{ - type:"string", - description: "Transaction id of the booking to be performed from the given list of bookings. It should not be set if th action is not from one of the bookings. It shold only be used when the action is 'search'" - } - } -} \ No newline at end of file diff --git a/server.js b/server.js index acc8ffa..eb07a8b 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,6 @@ dotenv.config() import express from 'express' import bodyParser from 'body-parser' import logger from './utils/logger.js' -import messageController from './controllers/Bot.js' import DBService from './services/DBService.js' import agentController from './controllers/Agent.js'; import { @@ -14,6 +13,7 @@ import { triggerExceptionOnLocation, updateStatus, unpublishItem, + webhookControl } from './controllers/ControlCenter.js' import path from 'path' import { fileURLToPath } from 'url'; @@ -35,7 +35,8 @@ app.post('/update-catalog', updateCatalog) app.post('/trigger-exception', triggerExceptionOnLocation) app.post('/update-status', updateStatus) app.post('/unpublish-item', unpublishItem) -app.post('/webhook-ps', messageController.webhookControl) +app.post('/webhook-ps', webhookControl) + // Reset all sessions export const db = new DBService() diff --git a/services/AI.js b/services/AI.js index 0f5bcad..10a48b8 100644 --- a/services/AI.js +++ b/services/AI.js @@ -1,18 +1,17 @@ import { readFileSync } from 'fs'; import OpenAI from 'openai' import logger from '../utils/logger.js' -import yaml from 'js-yaml' import { v4 as uuidv4 } from 'uuid' -import actions from '../schemas/jsons/actions.js'; import Actions from './Actions.js'; import { TOOLS } from '../config/GPT/tools.js'; // TODO: Load schemas. This needs to be improved so that any new schema is automatically loaded -import search from '../schemas/jsons/search.js'; -import select from '../schemas/jsons/select.js'; -import init from '../schemas/jsons/init.js'; -import confirm from '../schemas/jsons/confirm.js'; +import search from '../config/schemas/search.js'; +import select from '../config/schemas/select.js'; +import init from '../config/schemas/init.js'; +import confirm from '../config/schemas/confirm.js'; import get_text_by_key from '../utils/language.js'; +import { EMPTY_SESSION } from '../config/constants.js'; const BECKN_ACTIONS = { search: { schema : search, call_to_action : "Which one would you like to select?" @@ -32,7 +31,6 @@ const NUMBER_OF_RETRIES=3; const openai = new OpenAI({ apiKey: process.env.OPENAI_AI_KEY, }) -const openai_config = JSON.parse(readFileSync('./config/openai.json')) const registry_config = JSON.parse(readFileSync('./config/registry.json')) class AI { @@ -42,549 +40,83 @@ class AI { this.action = null; this.bookings = []; this.actionService = new Actions(); - this.session = {}; + this.session = EMPTY_SESSION; this.tools = []; this.attempt = 0; // for API call attempts } - async get_beckn_action_from_text(instruction, context=[], last_action=null){ - logger.info(`Getting action from instruction : ${instruction} and last_action is ${last_action}, context is ${JSON.stringify(context)}`); - let response = { - action : null - } - const messages = [ - { role: 'system', content: `Supported actions : ${JSON.stringify(openai_config.SUPPORTED_ACTIONS)}` }, - { role: 'system', content: `Last action : ${last_action}` }, - ...context, - { role: "user", content: instruction } + /** + * This function takes a list of messages and returns the response from the AI model + * It also checks for tool calls and executes them if found + * @param {*} messages | array of messages to be sent to the AI model in the format [{role: 'user', content: 'message'}, {role: 'assistant', content: 'message'}] + * @param {*} raw_yn | If this paramater is true, the function will return raw json response for tool calls + * @returns + */ + async get_response_or_perform_action(messages, raw_yn=false){ - ]; - - const tools = [ - { - type: "function", - function: { - name: "get_action", - description: "Identify if the user wants to perform an action.", - parameters: actions - } - } - ]; + if(messages.length==0) return false; // no messages to process + + const context = [ + {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, + {role: 'assistant', content : "You can only share results immediately, so you should never say that you will do something in the future. "}, + {role: 'assistant', content : "If the last tool call did not produce any useful response, you should convey that directly."}, + {role: 'assistant', content : "Your tone should be polite and helpful. "}, + ] try{ const gpt_response = await openai.chat.completions.create({ model: process.env.OPENAI_MODEL_ID, - messages: messages, - tools: tools, + messages: [...context, ...messages], + tools: TOOLS, tool_choice: "auto", }); - response = JSON.parse(gpt_response.choices[0].message?.tool_calls[0]?.function?.arguments) || response; - if(!response.action) response.action = null; - logger.info(`Got the action : ${JSON.stringify(response)}`); - return response - } - catch(e){ - logger.error(e); - return response; - } - } - - /** - * Get response for general query - * @param {*} instruction - * @param {*} context - * @returns - */ - async get_ai_response_to_query(instruction, context=[], profile = {}){ - - const openai_messages = [ - { role: 'system', content: 'If you are asked to prepare an itinerary or plan a trip, you should have information about the user preferences such as journey dates, journey destination, number of members, mode of transport etc.'}, - { role: 'system', content: 'You must come back with a response immedietaley, do not respond back saying that you will come back with a resopnse.'}, - { role: 'system', content: `User profile : ${JSON.stringify(profile)}`}, - ...context, - { role: 'user', content: instruction} - ] - - let response = { - action: null, - response: null - } - try{ - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - max_tokens: 300 - }) - response = completion.choices[0].message.content; - } - catch(e){ - logger.error(e); - } - - logger.info(`Got response from AI for a general query : ${response}`) - return response; - } + let responseMessage = gpt_response.choices[0].message; - /** - * Get the right schema for a given action - * @returns - */ - async get_schema_by_action() { - let schema = false; - - if(this.action?.action){ - try { - const filePath = `./schemas/core_1.1.0/${this.action?.action}.yml`; - schema = yaml.load(readFileSync(filePath, 'utf8')); - - } catch (error) { - logger.error(error); - } - } - else{ - logger.error(`No action found in the instance.`); - } - - logger.info(`Found schema for action : ${this.action?.action}`) - return schema; - } - - /** - * Get beckn context for a given instruction - * @param {*} instruction - * @param {*} context - * @returns - */ - async get_context_by_instruction(instruction, session){ - - const desired_structure = { - domain:`DOMAIN_AS_PER_REGISTRY_AND_INSTRUCTION_GIVEN_BY_USER` - } - - let last_action_context=[]; - if(this.action?.action!=='search'){ - desired_structure.bpp_id = ``; - desired_structure.bpp_uri = ``; - - // last action context - if(session?.profile?.last_action && session.beckn_transaction?.responses[`on_${session.profile.last_action}`]){ - last_action_context = [ - {role: 'system', content: `Response of last action '${session.profile.last_action}' is : ${JSON.stringify(session.beckn_transaction?.responses[`on_${session.profile.last_action}`])}`}, - ] - } - } - - let response = { - message_id : uuidv4(), - transaction_id: uuidv4(), - base_url: registry_config[0].url, - bap_id: registry_config[0].bap_subscriber_id, - bap_uri: registry_config[0].bap_subscriber_url, - action: this.action?.action, - version: registry_config[0].version, - - } - if(this.action.transaction_id && this.action.transaction_id!=='' && this.action.transaction_id!==null){ - response.transaction_id = this.action.transaction_id; - } - - const openai_messages = [ - { role: 'system', content: `Your job is to analyse the given instruction, registry details and generate a config json in the following structure : ${JSON.stringify(desired_structure)}` }, - { role: 'system', content: `Registry : ${JSON.stringify(registry_config)}` }, - { role: 'system', content: `Instruction : ${instruction}` }, - ...last_action_context, - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - let gpt_response = JSON.parse(completion.choices[0].message.content) - response = {...response, ...gpt_response}; - logger.info(`Got context from instruction : ${JSON.stringify(response)}`); - return response; - } catch (e) { - logger.error(e) - return {} - } - } - - /** - * Get beckn payload based on instruction, hostorical context, beckn context and schema - * @param {*} instruction - * @param {*} context - * @param {*} beckn_context - * @param {*} schema - * @returns - */ - async get_beckn_request_from_text(instruction, beckn_context={}, schema={}, session={}){ - - logger.info(`get_beckn_request_from_text() : ${instruction}, for schema : ${schema} , context : ${JSON.stringify(beckn_context)}`) - let action_response = { - status: false, - data: null, - message: null - } - - // last action context - let last_action_context = []; - if(session?.profile?.last_action && session.beckn_transaction?.responses[`on_${session.profile.last_action}`]){ - last_action_context = [ - {role: 'system', content: `Response of last action '${session.profile.last_action}' is : ${JSON.stringify(session.beckn_transaction?.responses[`on_${session.profile.last_action}`])}`}, - ] - } - let openai_messages = [ - ...openai_config.SCHEMA_TRANSLATION_CONTEXT, - { "role": "system", "content": `Schema definition: ${JSON.stringify(schema)}` }, - {"role": "system", "content": `User profile : ${JSON.stringify(session.profile)}`}, - ...last_action_context, - {"role": "system", "content": `Following is the conversation history`}, - ...session?.text?.slice(-3) || [], - { "role": "user", "content": instruction } - ] - - try{ - const completion =await openai.chat.completions.create({ - messages: openai_messages, - model: 'gpt-4-0125-preview', - response_format: { type: 'json_object' }, - temperature: 0, - }) - const jsonString = completion.choices[0].message.content.trim() - logger.info(`Got beckn payload`) - logger.info(jsonString) - logger.info(`\u001b[1;34m ${JSON.stringify(completion.usage)}\u001b[0m`) - - let response = JSON.parse(jsonString) - - // Corrections - response.body.context = { - ...response.body.context, - ...beckn_context - }; - response.url = `${beckn_context.base_url}/${beckn_context.action}` - - action_response = {...action_response, status: true, data: response} - } - catch(e){ - logger.error(e); - action_response = {...action_response, message: e.message} - } - - - return action_response; - } - - async get_beckn_message_from_text(instruction, context=[], domain='', polygon=null) { - logger.info(`get_beckn_message_from_text() : ${instruction}, for domain : ${domain} , polygon : ${polygon}`) - let domain_context = [], policy_context = []; - if(domain && domain!='') { - domain_context = [ - { role: 'system', content: `Domain : ${domain}`} - ] - if(registry_config[0].policies.domains[domain]){ - policy_context = [ - { role: 'system', content: `Use the following policy : ${JSON.stringify(registry_config[0].policies.domains[domain])}` } - ] - } - } - - const messages = [ - ...policy_context, - ...domain_context, - ...context, - { role: "user", content: instruction } - - ]; + // check for tool calls + const toolCalls = responseMessage.tool_calls; + if (toolCalls) { + logger.info("Tool calls found in response, proceeding..."); - const tools = [ - { - type: "function", - function: { - name: "get_search_intent", - description: "Get the correct search object based on user inputs", - parameters: search - } - } - ]; - try{ - // Assuming you have a function to abstract the API call - const response = await openai.chat.completions.create({ - model: 'gpt-4-0125-preview', - messages: messages, - tools: tools, - tool_choice: "auto", // auto is default, but we'll be explicit - }); - const responseMessage = JSON.parse(response.choices[0].message?.tool_calls[0]?.function?.arguments) || null; - logger.info(`Got beckn message from instruction : ${JSON.stringify(responseMessage)}`); - if(this.action?.action=='search' && responseMessage?.intent?.fulfillment?.stops[0]?.location){ - if(polygon){ - responseMessage.intent.fulfillment.stops[0].location.polygon = polygon; - const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300&path=color:%231d3b65|weight:5|enc:${polygon}&key=${process.env.GOOGLE_MAPS_API_KEY}`; - logger.info(`Map url for request : ${route_image}`) - // temp - delete responseMessage.intent.fulfillment.stops[0].location.gps; - } - else delete responseMessage.intent.fulfillment.stops[0].location.polygon; + messages.push(responseMessage); - } - if(this.action?.action=='search' && responseMessage?.intent?.fulfillment?.stops && responseMessage?.intent?.fulfillment?.stops.length>0){ - responseMessage?.intent?.fulfillment?.stops.map(stop=>{ - if(stop?.time?.timestamp){ - stop.time.timestamp = new Date('2024-04-12T12:00:00Z').toISOString(); - } - }) - } - - return responseMessage - } - catch(e){ - logger.error(e); - return null; - } - } + for (let tool of toolCalls) { + logger.warn(`Executing tool : ${tool.function.name} ...`); + const parameters = JSON.parse(tool.function.arguments); + const functionToCall = this.tools[tool.function.name]; + if (functionToCall) { + const response = await functionToCall(parameters); + + messages.push({ + tool_call_id: tool.id, + role: "tool", + name: functionToCall, + content: JSON.stringify(response), + }); - async compress_search_results(search_res){ - - const desired_output = { - "providers": [ - { - "id": "some_provider_id", - "name": "some_provider_name", - "bpp_id": "some_bpp_id", - "bpp_uri": "some_bpp_uri", - "items": [ - { - "id": "some_item_id", - "name": "some_item_name" + // call again to get the response + responseMessage = await this.get_response_or_perform_action(messages); + if(raw_yn) { + responseMessage.raw = response.data; } - ] + } } - ] - } - let openai_messages = [ - { "role" : "system", "content": `Your job is to complress the search results received from user into the following JSON structure : ${JSON.stringify(desired_output)}`}, - { "role" : "system", "content": "bpp_id and bpp_uri for a provide must be picked up from its own context only." }, - { "role" : "system", "content": "you should not use responses or providers that do not have items." }, - { "role": "user", "content": JSON.stringify(search_res)} - ] - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, // Using bigger model for search result compression - response_format: { type: 'json_object' }, - temperature: 0, - }) - const jsonString = completion.choices[0].message.content.trim() - logger.info(jsonString) - logger.info(`\u001b[1;34m ${JSON.stringify(completion.usage)}\u001b[0m`) - - const compressed = JSON.parse(jsonString) - return {...search_res, responses: compressed}; - } - - - async format_response(json_response, context=[], profile={}) { - const desired_output = { - status: true, - message: "" - }; - - let call_to_action = { - 'search': 'You should ask which item the user wants to select to place the order. You should show search results in a listing format with important details mentioned such as name, price, rating, location, description or summary etc. and a call to action to select the item.', - 'select': 'You should ask if the user wants to initiate the order. You should not use any links from the response.', - 'init': 'You should ask if the user wants to confirm the order. ', - 'confirm': 'You should display the order id and show the succesful order confirmation message. You should ask if the user wants to book something else.', - } - - if(!(profile.phone && profile.email && profile.name)){ - call_to_action.select+= 'Billing details are mandatory for initiating the order. You should ask the user to share billing details such as name, email and phone to iniatie the order.'; - } - - const openai_messages = [ - {role: 'system', content: `Your job is to analyse the input_json and provided chat history to convert the json response into a human readable, less verbose, whatsapp friendly message and return this in a json format as given below: \n ${JSON.stringify(desired_output)}.`}, - {role: 'system', content: `${call_to_action[json_response?.context?.action] || 'you should ask the user what they want to do next.'}`}, - // {role: 'system', content: `If the given json looks like an error, summarize the error but for humans, do not include any code or technical details. Produce some user friendly fun messages.`}, - // {role: 'system', content: `User profile : ${JSON.stringify(profile)}`}, - {role: 'assistant',content: `input_json: ${JSON.stringify(json_response)}`}, - {role: 'system', content: `Chat history goes next ....`}, - ...context.slice(-2), - ] - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - let response = JSON.parse(completion.choices[0].message.content) - return response; - } catch (e) { - logger.error(e) - return { - status:false, - message:e.message } - } - - } - - async get_profile_from_text(message, profile={}){ - const desired_output = { - "name": "", - "email": "", - "phone": "", - "travel_source": "", - "travel_destination": "", - "current_location_gps": "", - "vehicle-type":"", - "connector-type": "", - "pet-friendly_yn":0, - "ev-charging-yn":0, - "accomodation_type":"", - "number_of_family_members":"" - } - - const openai_messages = [ - { role: 'system', content: `Please analyse the given user message and extract profile information about the user which is not already part of their profile. The desired outout format should be the following json ${JSON.stringify(desired_output)}` }, - { role: 'system', content: `You must not send any vague or incomplete information or anything that does not tell something about the user profile.` }, - { role: 'system', content: `Return empty json if no profile information extracted.` }, - { role: 'system', content: `Existing profile : ${JSON.stringify(profile)}`}, - { role: 'user', content: message } - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - let response = JSON.parse(completion.choices[0].message.content) - return { - status: true, - data: response - }; - } catch (e) { - logger.error(e) - return { - status:false, - message:e.message - } - } - } - - async check_if_booking_collection(message, context=[]){ - let response = false; - const openai_messages = [ - { role: 'system', content: `Your job is to identify if the given user input is an instruction to make multiple bookings at once.` }, - { role: 'system', content: `you must return a json object with the following structure {status: true/false}` }, - { role: 'system', content: `Status must be true if the given user message is a request to make multiple bookings and the last assistant message is an itinerary or a plan. For e.g. if the assistant had shared the itinerary/plan in context and user says 'lets make the bookings' or 'make all bookings', or 'Can you make the bookings?', status should be true` }, - { role: 'system', content: `Status should be false if its not a clear instrcution to make multiple bookings. For r.g. if the user shares details about the trip, its not a clear instrcution to make bookings.` }, - { role: 'system', content: `Status should be false if the assistant has asked to select, initiate or confirm an order.` }, - { role: 'system', content: `Context goes here...` }, - ...context, - { role: 'user', content: message } - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - let gpt_response = JSON.parse(completion.choices[0].message.content) - response = gpt_response.status; - } catch (e) { - logger.error(e) - } - return response; - } - - async get_bookings_array_from_text(message){ - let bookings = []; - const desired_output = [ - { - "name": "Hotel at xyz", - "booked_yn": 0 - } - ] - const openai_messages = [ - { role: 'system', content: `You must convert the given list of bookings into the desired json array format : ${JSON.stringify(desired_output)}` }, - { role: 'user', content: message } - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - bookings = JSON.parse(completion.choices[0].message.content) - } catch (e) { - logger.error(e) - - } - - logger.info(`Got bookings array : ${JSON.stringify(bookings)}`) - return bookings; - } - - async get_bookings_status(bookings, context){ - let bookings_updated = []; - const openai_messages = [ - { role: 'system', content: `You must check the given list of bookings and the context history and mark the status booked_yn = 0 or 1 depending upon wether that booking has been done in the context or not.` }, - { role: 'system', content: `A booking should be considered as done if it has been confirmed or user has indicated to not book that particular item. For e.g. if the booking name is 'Accomodation at xyz' and a booking at xyz has been done in the context, its status should be marked as 1` }, - { role: 'system', content: `You must return a json array with the same format as bookings.` }, - { role: 'system', content: `Context goes here...` }, - ...context, - { role: 'user', content: `Bookings : ${JSON.stringify(bookings)}` } - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - bookings_updated = JSON.parse(completion.choices[0].message.content) - } catch (e) { - logger.error(e) + return responseMessage; } - return bookings_updated; - } - - 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 : ${desired_output}` }, - ...context, - { role: 'user', content: message } - ] - - try { - const completion = await openai.chat.completions.create({ - messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, - temperature: 0, - response_format: { type: 'json_object' }, - }) - let response = JSON.parse(completion.choices[0].message.content) - return response; - } catch (e) { - logger.error(e) - return {}; - } + catch(e){ + logger.error(e); + return false; + } } + /** + * This function performs a beckn transaction by calling the beckn API and returns the response form API call + * @param {*} param0 | action and instruction + * @returns + */ async perform_beckn_transaction({action, instruction}){ logger.info(`perform_beckn_transaction() : ${action}, ${instruction}`); let response = { @@ -808,66 +340,7 @@ class AI { } return message; - } - - async getResponseFromOpenAI(messages, raw_yn=false){ - - const context = [ - {role: 'assistant', content : "You are a travel planner ai agent that is capable of performing actions. "}, - {role: 'assistant', content : "You can only share results immediately, so you should never say that you will do something in the future. "}, - {role: 'assistant', content : "If the last tool call did not produce any useful response, you should convey that directly."}, - {role: 'assistant', content : "Your tone should be polite and helpful. "}, - - ] - try{ - const gpt_response = await openai.chat.completions.create({ - model: process.env.OPENAI_MODEL_ID, - messages: [...context, ...messages], - tools: TOOLS, - tool_choice: "auto", - }); - let responseMessage = gpt_response.choices[0].message; - - // check for tool calls - const toolCalls = responseMessage.tool_calls; - if (toolCalls) { - logger.info("Tool calls found in response, proceeding..."); - - - messages.push(responseMessage); - - for (let tool of toolCalls) { - logger.warn(`Executing tool : ${tool.function.name} ...`); - const parameters = JSON.parse(tool.function.arguments); - const functionToCall = this.tools[tool.function.name]; - if (functionToCall) { - const response = await functionToCall(parameters); - - messages.push({ - tool_call_id: tool.id, - role: "tool", - name: functionToCall, - content: JSON.stringify(response), - }); - - // call again to get the response - responseMessage = await this.getResponseFromOpenAI(messages); - if(raw_yn) { - responseMessage.raw = response.data; - } - } - } - } - - return responseMessage; - - } - catch(e){ - logger.error(e); - return false; - } - } - + } } diff --git a/services/Actions.js b/services/Actions.js index fef5b89..576c517 100644 --- a/services/Actions.js +++ b/services/Actions.js @@ -1,7 +1,6 @@ import twilio from 'twilio' import logger from '../utils/logger.js' import axios from 'axios' -import AI from './AI.js' import {createWriteStream} from 'fs' import path from 'path' import { v4 as uuidv4 } from 'uuid' diff --git a/services/Conversation.js b/services/Conversation.js deleted file mode 100644 index cafe3bb..0000000 --- a/services/Conversation.js +++ /dev/null @@ -1,15 +0,0 @@ -class Conversation{ - constructor(session) { - this.session = session; - } - - async getResponse(message) { - let response = {}; - - // get answer from AI - - return response; - } -} - -export default Conversation; \ No newline at end of file diff --git a/services/MapService.js b/services/MapService.js index 4e9238a..af6cab1 100644 --- a/services/MapService.js +++ b/services/MapService.js @@ -1,10 +1,6 @@ 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'; -import get_text_by_key from "../utils/language.js"; - class MapsService { constructor() { @@ -93,57 +89,6 @@ class MapsService { return encodeURIComponent(color); } - async generate_routes(message, context=[], avoid_point=[]) { - 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(get_text_by_key('missing_source')); - } - if (!details.destination) { - response.errors.push(get_text_by_key('missing_destination')); - } - } - else{ - - // generate routes - const routes = await this.getRoutes({...details, avoidPoint: avoid_point}); - response.data.routes = routes.map(route=>{ - return { - overview_polyline: route.overview_polyline, - summary: route.summary, - source_gps: details.source, - destination_gps: details.destination - } - }) - - // print path - const path = this.get_static_image_path(routes) - logger.info(`Route image path : ${path}`) - - response.data.routes_formatted = { - "description": get_text_by_key('route_list_description'), - "routes": response.data.routes.map((route, index) => `Route ${index+1}: ${route.summary}\n`) - } - response.status = true; - } - - // logger.info(`Generated routes response : ${JSON.stringify(response, null, 2)}`); - return response; - } - /** * Check if a GPS point is on a polyline * diff --git a/tests/apis/agent.test.js b/tests/apis/agent.test.js deleted file mode 100644 index 2c4660c..0000000 --- a/tests/apis/agent.test.js +++ /dev/null @@ -1,252 +0,0 @@ -import { describe, it } from 'mocha' -import app from '../../server.js' -import request from 'supertest' -import * as chai from 'chai' -import logger from '../../utils/logger.js' -import DBService from '../../services/DBService.js' -const expect = chai.expect - -before(async () => { - // Reset all sessions - const db = new DBService() - await db.clear_all_sessions() -}) - -describe.skip('API tests for /webhook endpoint for an end to end search > select > init > confirm use case', () => { - it('Should test succesful search response using /webhook endpoint', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "I'm looking for ev chargers near me", - }) - - expect(response.status).equal(200) - expect(response.text).to.contain('ChargeZone.in') - }) - - it('Should test successful select response using /webhook endpoint', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: 'I would like to select the first one', - }) - - expect(response.status).equal(200) - expect(response.text).to.contain('ChargeZone.in') - }) - - it('Should test successful init response using /webhook endpoint', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: 'I would like to place an order. My details are : John Doe, 9999999999, test@example.com', - raw_yn: true - }) - - expect(response.status).equal(200) - expect(response.body.responses[0].message.order.fulfillments[0]).to.have.property('id') - - }) - - it('Should test successful confirm response using /webhook endpoint', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: 'Lets confirm!', - raw_yn: true - }) - - expect(response.status).equal(200) - expect(response._body.responses[0].message.order).to.have.property('id') - }) -}) - - -describe('Test cases for trip planning workflow', ()=>{ - it('Should test succesful trip planning intent', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "I'm planing a trip from Denver to Yellowstone national park", - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should return a trip after sharing details.', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "Sure, I'm planning the trip on April 12th, I'm travelling with my family of 4. I also have a shihtzu dog. I have an EV vehicle, want to stay 1 day at the national park. I am a vegan. I want to stay near Casper 1 day to take a break.", - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should return search results when asked to look for hotels.', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "Okay, lets find some hotels near Yellowstone National Parkr", - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should select a hotel when asked.', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "Lets select the first one.", - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should init a hotel order when asked.', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "lets initiate the order, My details are : John Doe, 9999999999, test@example.com" - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should init a hotel order when asked.', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "Lets confirm!" - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) - - it('Should try and find another hotel', async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: "Lets confirm!" - }) - - expect(response.status).equal(200) - expect(response.text).to.be.a('string') - }) -}) - -describe.skip('Test cases for booking collection', ()=>{ - it('Should make the hotel bookings', async ()=>{ - - const chats = [ - "Hey Alfred, you up? ", - "Iโ€™m hitting Yellowstone National Park on April 12th for 2 days. Find a hotel to book", - "First one sounds awesome. Lets go with that", - "Sure, John A. Smith, john.smith@example.com, (555) 123-4567", - "Sure, lets confirm!", - "Can you share the best routes from Denver to Yellowstone national park?", - "Lets go with Route 1", - "Can you find some ev chargers along the way?", - "Lets go with the first one", - "go ahead and initiate!", - "Sure, lets confirm" - ]; - - - for(const chat of chats){ - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: chat, - raw_yn : 1 - }); - logger.info(JSON.stringify(response.text, null, 2)); - expect(response.status).equal(200); - } - }) - - it('Should trigger an exception', async ()=>{ - const response = await request(app).post('/update-status').send({ - "From": process.env.TEST_RECEPIENT_NUMBER, - "Body": "I'm looking for some ev chargers along my route. Im' currently near Casper." - }) - logger.info(JSON.stringify(response.text, null, 2)); - expect(response.status).to.be.eq(200) - }) - - it.skip('Should Place the order for raincoats', async ()=>{ - - const chats = [ - "Can you find some raincoats near Yellwostone national park?", - "Lets get the first one", - "Sure, go ahead and place the order", - "Lets confirm the order", - - ]; - - for(const chat of chats){ - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: chat, - }); - logger.info(JSON.stringify(response.text, null, 2)); - expect(response.status).equal(200); - } - }) -}) - -describe('test cases for generating routes', ()=>{ - 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) - - }) -}) - -describe.skip('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?"; - 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 select a route and share directions.', async () => { - const ask = "Lets select the first route."; - 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 search along a route.', async () => { - const ask = "I'm looking for some ev chargers along my route. Im' currently near Casper."; - 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) - - }) - - -}) \ No newline at end of file diff --git a/tests/apis/apis.test.js b/tests/apis/apis.test.js new file mode 100644 index 0000000..eb2f0ec --- /dev/null +++ b/tests/apis/apis.test.js @@ -0,0 +1,34 @@ +import { describe, it } from 'mocha' +import app from '../../server.js' +import request from 'supertest' +import * as chai from 'chai' +import logger from '../../utils/logger.js' +import DBService from '../../services/DBService.js' +const expect = chai.expect + +before(async () => { + // Reset all sessions + const db = new DBService() + await db.clear_all_sessions() +}) + +describe('API tests for a simple order confirmation workflow', ()=>{ + const chats = [ + {key: "search_hotel", value: "Can you please find hotels near Yellowstone national park?"}, + {key: "select_hotel", value: "Lets select the first one."}, + {key: "initiate_order", value: "Lets initiate the order. My details are : John Doe, 1234567890, john.doe@example.com"}, + {key: "confirm_order", value: "Lets confirm."} + ] + + for(const chat of chats){ + it(`Should return a response for ${chat.key}`, async () => { + const response = await request(app).post('/webhook').send({ + From: process.env.TEST_RECEPIENT_NUMBER, + Body: chat.value, + }) + logger.info(`Response for ${chat.key} : ${response.text}`) + expect(response.text).to.be.a('string'); + expect(response.text).to.contain('Lake'); + }) + } +}) \ No newline at end of file diff --git a/tests/unit/controllers/agent.test.js b/tests/unit/controllers/agent.test.js index 9bd380a..29b5e33 100644 --- a/tests/unit/controllers/agent.test.js +++ b/tests/unit/controllers/agent.test.js @@ -3,7 +3,6 @@ import * as chai from 'chai' const expect = chai.expect import request from 'supertest' import app from '../../../server.js' -import logger from '../../../utils/logger.js' describe('API tests for getResponse() function', () => { it('should return 400 if From or Body is missing', async () => { @@ -25,7 +24,7 @@ describe('API tests for getResponse() function', () => { expect(response.text).to.contain('New Delhi'); }) - it.only('Should return list of routes between two points if asked', async () => { + it('Should return list of routes between two points if asked', async () => { const message = "Can you share routes between New Delhi and Mumbai?" const response = await request(app).post('/webhook').send({ From: process.env.TEST_RECEPIENT_NUMBER, @@ -35,14 +34,13 @@ describe('API tests for getResponse() function', () => { expect(response.text).to.contain('NH 48'); }) - it.only('Should select a route', async () => { + it('Should select a route', async () => { const message = "Lets select the first one" const response = await request(app).post('/webhook').send({ From: process.env.TEST_RECEPIENT_NUMBER, Body: message, }) expect(response.text).to.be.a('string'); - expect(response.text).to.contain('first'); }) it('Should return a list of hotels', async () => { @@ -54,25 +52,4 @@ describe('API tests for getResponse() function', () => { expect(response.text).to.be.a('string'); expect(response.text).to.contain('Lake'); }) -}) - -describe('API tests for a order confirmation workflow', ()=>{ - const chats = [ - {key: "search_hotel", value: "Can you please find hotels near Yellowstone national park?"}, - {key: "select_hotel", value: "Lets select the first one."}, - {key: "initiate_order", value: "Lets initiate the order. My details are : John Doe, 1234567890, john.doe@example.com"}, - {key: "confirm_order", value: "Lets confirm."} - ] - - for(const chat of chats){ - it(`Should return a response for ${chat.key}`, async () => { - const response = await request(app).post('/webhook').send({ - From: process.env.TEST_RECEPIENT_NUMBER, - Body: chat.value, - }) - logger.info(`Response for ${chat.key} : ${response.text}`) - expect(response.text).to.be.a('string'); - expect(response.text).to.contain('Lake'); - }) - } }) \ No newline at end of file diff --git a/tests/unit/controllers/bot.test.js b/tests/unit/controllers/bot.test.js deleted file mode 100644 index f2c0a58..0000000 --- a/tests/unit/controllers/bot.test.js +++ /dev/null @@ -1,169 +0,0 @@ -import * as chai from 'chai' -import { describe } from 'mocha' -import AI from '../../../services/AI.js' -import MapsService from '../../../services/MapService.js' -import logger from '../../../utils/logger.js' -import ActionService from '../../../services/Actions.js' -import DBService from '../../../services/DBService.js' -const expect = chai.expect -const mapService = new MapsService() -const ai = new AI(); -const actionsService = new ActionService() - -describe('Test cases for AI', () => { - it.skip('Should return message with location polygon', async () => { - - const source_gps = await mapService.lookupGps('Denver'); - const destination_gps = await mapService.lookupGps('Yellowstone national park'); - - // generate routes - const routes = await mapService.getRoutes({source: source_gps, destination: destination_gps}); - - const context=[ - { role : 'user', content: `Selected route polygon is : ${routes[0].overview_polyline.points}`} - ] - let instruction = "I'm looking for some ev chargers along my route. My current location is 30.876877, 73.868969"; - let response = await ai.get_beckn_message_from_text(instruction, context, 'uei:charging') - logger.info(JSON.stringify(response, null, 2)); - expect(response).to.be.an('object'); - expect(response).to.have.property('intent'); - expect(response.intent).to.have.property('fulfillment'); - expect(response.intent.fulfillment.stops[0].location).to.have.property('polygon'); - }) - - -}) - - -describe('Test cases for Google maps', () => { - it.skip('Should Render a static map image based on source and destination', async ()=>{ - - const source ='37.422391,-122.084845'; - const destination = '37.411991,-122.079414'; - - let routes = await mapService.getRoutes({source: source, destination: destination}); - const selected_route = 0; - const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300&path=enc:${routes[selected_route].overview_polyline.points}&key=${process.env.GOOGLE_MAPS_API_KEY}`; - logger.info(`route_image: ${route_image}`); - expect(routes).to.be.an('array'); - }) - - it.skip('Should calculate route between an origin and a destination', async ()=>{ - - const source_gps = await mapService.lookupGps('Denver'); - const destination_gps = await mapService.lookupGps('Yelllowstone national park'); - - const directions = `https://www.google.com/maps/dir/${source_gps.lat},${source_gps.lng}/${destination_gps.lat},${destination_gps.lng}/`; - logger.info(`route_image: ${directions}`); - expect(source_gps).to.be.an('object'); - expect(source_gps).to.have.property('lat'); - expect(source_gps).to.have.property('lng'); - }) - - it('It should take a trip plannign input and generate static route image and directions link.', async () => { - const ask = "Can you plean a trip from Denver to Yellowstone national park?"; - - // identify source and destination - const format = { - 'source': 'SOURCE_LOCATION', - 'destination': 'DESTINATION_LOCATION' - } - - const details = await ai.get_details_by_description(ask, [], JSON.stringify(format)); - expect(details).to.have.property('source'); - expect(details).to.have.property('destination'); - - logger.info(JSON.stringify(details, null, 2)); - - // Get gps for source and destination - - const source_gps = await mapService.lookupGps(details.source); - const destination_gps = await mapService.lookupGps(details.destination); - - // generate routes - const routes = await mapService.getRoutes({source: source_gps, destination: destination_gps}); - - // Selected route - const selected_route = 0; - - // generate image and directions - const directions = `https://www.google.com/maps/dir/${source_gps.lat},${source_gps.lng}/${destination_gps.lat},${destination_gps.lng}/`; - const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300&path=enc:${routes[selected_route].overview_polyline.points}&key=${process.env.GOOGLE_MAPS_API_KEY}`; - - // const server_route_image = await actionsService.download_file(route_image); - await actionsService.send_message(process.env.TEST_RECEPIENT_NUMBER, `Here are the directions: ${directions}`); - logger.info(`directions: ${directions}`); - logger.info(`route_image: ${route_image}`); - expect(routes).to.be.an('array').that.is.not.empty; - }) - - it('It should be able to take a route as an input and turn it into a text response', async () => { - const source ='37.422391,-122.084845'; - const destination = '37.411991,-122.079414'; - - let routes = await mapService.getRoutes({source: source, destination: destination}); - let summary = 'Here are the available routes for your request: \n\n'; - routes.forEach((route, index) => { - summary+=`Route ${index+1}: ${route.summary}. \n`; - }) - logger.info(`Summary of routes: ${summary}`); - expect(summary).to.be.a('string'); - }) - - it('Should generate a static image with multiple routes for a given source and destination', async ()=>{ - const source = await mapService.lookupGps('Denver'); - const destination = await mapService.lookupGps('Yelllowstone national park'); - - let routes = await mapService.getRoutes({source: source, destination: destination}); - let polygon_path = ''; - routes.forEach((route, index) => { - polygon_path+=`&path=color:${mapService.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}`; - logger.info(`route_image: ${route_image}`); - expect(routes).to.be.an('array'); - }) - - it('Should generate a static image with selected route and multiple', async ()=>{ - const source = await mapService.lookupGps('Denver'); - const destination = await mapService.lookupGps('Yelllowstone national park'); - const items=[ - {name: 'Weld county - charging station', gps: '41.142558,-104.839765๎”ฎ'}, - {name: 'Natrony county - EV', gps: '42.958550,-106.651372'}, - ] - - // gerenate routes - let routes = await mapService.getRoutes({source: source, destination: destination}); - let selected_route = routes[0]; - let polygon_path = `&path=color:${mapService.get_random_color()}|enc:${selected_route.overview_polyline.points}`;; - - // get markers - let markers_path = ''; - items.forEach((item) => { - markers_path+=`&markers=color:blue|label:${encodeURIComponent(item.name)}|${item.gps}`; - }) - - markers_path=`&markers=color:red|label:LABEL1|41.142558,-104.839765๎”ฎ - &markers=color:blue|label:LABEL2|42.958550,-106.651372` - - const route_image = `https://maps.googleapis.com/maps/api/staticmap?size=300x300${polygon_path}${markers_path}&key=${process.env.GOOGLE_MAPS_API_KEY}`; - logger.info(`route_image: ${route_image}`); - expect(routes).to.be.an('array'); - }) - - it('Should test if the polygon saved and polygon received are same', async ()=>{ - const db = new DBService() - const source ='37.422391,-122.084845'; - const destination = '37.411991,-122.079414'; - - let routes = await mapService.getRoutes({source: source, destination: destination}); - const polygon = routes[0].overview_polyline.points; - await db.update_session('123', { - polygon: polygon - }) - - await db.get_session('123') - }) -}) - diff --git a/tests/unit/controllers/controlCenter.test.js b/tests/unit/controllers/controlCenter.test.js index 23ac465..1567f91 100644 --- a/tests/unit/controllers/controlCenter.test.js +++ b/tests/unit/controllers/controlCenter.test.js @@ -87,7 +87,7 @@ describe('API tests for /update-catalog endpoint for an end to end Notify Messag }) describe('API tests for triggering a roadblock', ()=>{ - it.skip('Should trigger a roadblock on a selected route', async ()=>{ + it('Should trigger a roadblock on a selected route', async ()=>{ 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!" diff --git a/tests/unit/services/actions.test.js b/tests/unit/services/actions.test.js index 3e90d23..038b9d7 100644 --- a/tests/unit/services/actions.test.js +++ b/tests/unit/services/actions.test.js @@ -11,17 +11,17 @@ describe('should test send_message()', () => { const message = "hi, this is a test message"; let status = await actionsService.send_message(recipient, message); - + expect(status.deliveryStatus).to.not.equal('failed') }); - + it('should test send a message via Twilio with a whatsapp prefix', async () => { const recipient = `whatsapp:${process.env.TEST_RECEPIENT_NUMBER}`; const message = "hi, this is a test message"; let status = await actionsService.send_message(recipient, message); expect(status.deliveryStatus).to.not.equal('failed') - + }); it('should throw an error for invalid recipient', async () => { @@ -45,7 +45,7 @@ describe('should test send_message()', () => { expect(error).to.be.an.instanceOf(Error); } }); - + it('Should test sending a message with media', async () => { const recipient = process.env.TEST_RECEPIENT_NUMBER; const message = 'This is an image'; diff --git a/tests/unit/services/ai.test.js b/tests/unit/services/ai.test.js index b3e1927..c3d4bbf 100644 --- a/tests/unit/services/ai.test.js +++ b/tests/unit/services/ai.test.js @@ -1,335 +1,69 @@ import * as chai from 'chai' const expect = chai.expect import AI from '../../../services/AI.js' -import { readFileSync } from 'fs'; -import { EMPTY_SESSION } from '../../../config/constants.js'; const ai = new AI(); -const on_search = JSON.parse(readFileSync('./tests/data/api_responses/on_search.json')) -const on_select = JSON.parse(readFileSync('./tests/data/api_responses/on_select.json')) -const on_init = JSON.parse(readFileSync('./tests/data/api_responses/on_init.json')) -const on_confirm = JSON.parse(readFileSync('./tests/data/api_responses/on_confirm.json')) -const registry_config = JSON.parse(readFileSync('./config/registry.json')) -const trip_planning = JSON.parse(readFileSync('./tests/data/chats/trip_planning.json')) -describe.skip('Test cases for services/ai/get_beckn_action_from_text()', () => { - it('Should return null action when shared details about a trip', async () => { - const response = await ai.get_beckn_action_from_text(trip_planning.TRIP_DETAILS); - expect(response).to.have.property('action') - expect(response.action).to.be.null - }) - - it('Should return null action when asked about list of bookings to be done', async () => { - const response = await ai.get_beckn_action_from_text(trip_planning.BOOKINGS_QUERY); - expect(response).to.have.property('action') - expect(response.action).to.be.null - }) - - it('Should return search action when asked about booking a hotel', async () => { - const response = await ai.get_beckn_action_from_text(trip_planning.FIND_HOTEL); - expect(response).to.have.property('action') - expect(response.action).to.equal('search') - }); - - it('Should return select action when user selects an item', async () => { - const context = [ - {role: 'user', content: trip_planning.FIND_HOTEL}, - {role: 'assistant', content: trip_planning.FIND_HOTEL_RESPONSE} - ]; - const response = await ai.get_beckn_action_from_text(trip_planning.SELECT_HOTEL, context); - expect(response).to.have.property('action') - expect(response.action).to.equal('select') - }); - - it('Should return init action when user tried to place an order', async () => { - const context = [ - {role: 'user', content: trip_planning.FIND_HOTEL}, - {role: 'assistant', content: trip_planning.FIND_HOTEL_RESPONSE}, - {role: 'user', content: trip_planning.SELECT_HOTEL}, - {role: 'assistant', content: trip_planning.SELECT_HOTEL_RESPONSE} - ]; - - const response = await ai.get_beckn_action_from_text(trip_planning.INIT_HOTEL, context); - expect(response).to.have.property('action') - expect(response.action).to.equal('init') - }); - - it('Should return confirm action when user tried to confirm an order', async () => { - const context = [ - {role: 'user', content: trip_planning.FIND_HOTEL}, - {role: 'assistant', content: trip_planning.FIND_HOTEL_RESPONSE}, - {role: 'user', content: trip_planning.SELECT_HOTEL}, - {role: 'assistant', content: trip_planning.SELECT_HOTEL_RESPONSE} - ]; - - const response = await ai.get_beckn_action_from_text(trip_planning.CONFIRM_HOTEL, context); - expect(response).to.have.property('action') - expect(response.action).to.equal('confirm') - }); - - it('Should return null action when user used the word `confirm` for someothing else', async () => { - const response = await ai.get_beckn_action_from_text('Can you confirm whats the latest time?'); - expect(response).to.have.property('action') - expect(response.action).to.be.null - }); -}) - -describe.skip('Test cases for get_ai_response_to_query() function', () => { - it('Should return a response with success false for an empty query', async () => { - const response = await ai.get_ai_response_to_query('Hi'); - expect(response).to.be.an.string; - }) - - it('Should return a response with success true for a valid query', async () => { - const profile = { - name: 'John Doe', - email: 'john.doe@example.com', - phone: '9999999999' - } - const response = await ai.get_ai_response_to_query(`Just bought a new EV that i wanted to take out for a spin. I'm thinking, Yellowstone this weekend. Are you up for it?`, [], profile); - expect(response).to.be.an.string; - }) -}) - -describe.skip('Test cases for get_schema_by_action() function', () => { - it('Should return a search schema for search', async () => { - ai.action = {action: 'search'}; - const response = await ai.get_schema_by_action(`I'm looking for some hotels`); - expect(response.paths).to.have.property('/search') - }) - - it('Should return false if no action found', async () => { - ai.action = null; - const response = await ai.get_schema_by_action(`I'm looking for some hotels`); +describe(`Test cases get_response_or_perform_action()`, ()=> { + it('Should fail if empty messages sent', async () => { + const messages = []; + const response = await ai.get_response_or_perform_action(messages); expect(response).to.be.false; }) - it('Should return false if invalid action found', async () => { - ai.action = {action: 'invalid'}; - const response = await ai.get_schema_by_action(`I'm looking for some hotels`); - expect(response).to.be.false; - }) -}) - -describe.skip('Test cases for get_context_by_instruction()', async () => { - it('Should return right config for search action in ev context', async () => { - ai.action = {action: 'search'}; - const config = await ai.get_context_by_instruction("I'm looking for ev:chargers");; - expect(config).to.have.property('action') - expect(config.action).to.equal('search'); - expect(config.domain).to.equal('uei:charging'); - expect(config.version).to.equal(registry_config[0].version); - expect(config.bap_id).to.equal(registry_config[0].bap_subscriber_id); - expect(config.bap_url).to.equal(registry_config[0].bpp_subscriber_uri); - }) - - it('Should return right config for search action in hospitality contect', async () => { - ai.action = {action: 'search'}; - const config = await ai.get_context_by_instruction("Okay, lets find some hotels near Yellowstone National Park"); - expect(config).to.have.property('action') - expect(config.action).to.equal('search'); - expect(config.domain).to.equal('hospitality'); - expect(config.version).to.equal(registry_config[0].version); - expect(config.bap_id).to.equal(registry_config[0].bap_subscriber_id); - expect(config.bap_url).to.equal(registry_config[0].bpp_subscriber_uri); + it('Should resopnd correctly for a general query', async () => { + const messages = [ + {role : 'user', content: "What is the capital of India?"} + ]; + const response = await ai.get_response_or_perform_action(messages); + expect(response).to.be.an('object'); + expect(response.content).to.contain('New Delhi'); }) - it('Should return right config for search action in retail context', async () => { - ai.action = {action: 'search'}; - const config = await ai.get_context_by_instruction("I'm looking for some pet food");; - expect(config).to.have.property('action') - expect(config.action).to.equal('search'); - expect(config.domain).to.equal('retail:1.1.0'); - expect(config.version).to.equal(registry_config[0].version); - expect(config.bap_id).to.equal(registry_config[0].bap_subscriber_id); - expect(config.bap_url).to.equal(registry_config[0].bpp_subscriber_uri); - }) + // Not adding more test cases as similar test cases are already covered in the controller tests }) -describe.skip('Test cases for services/ai/compress_search_results()', () => { - it('Should test succesful compression of search results', async () => { - const compressed = await ai.compress_search_results(on_search); - expect(compressed.responses.providers).to.be.an('array') - expect(compressed.responses.providers.length).to.be.greaterThan(0) - expect(compressed.responses.providers[0]).to.have.property('id') - expect(compressed.responses.providers[0]).to.have.property('name') - expect(compressed.responses.providers[0]).to.have.property('items'); - }) -}) - -describe.skip('Test cases for services/ai/get_beckn_request_from_text()', () => { - let session = EMPTY_SESSION; - - it('Should test get_beckn_request_from_text() succesfully for a `search` intent', async () => { - - ai.action = {action: 'search'}; - const schema = await ai.get_schema_by_action(); - const response = await ai.get_beckn_request_from_text(trip_planning.FIND_HOTEL, on_search.context, schema, session); - expect(response.status).to.be.eq(true); - expect(response.data).to.be.an('object') - expect(response.data.method.toUpperCase()).to.be.eq('POST') - expect(response.data.url).to.contain('search') - expect(response.data.body.message.intent.descriptor).to.have.property('name') - session.profile.last_action = ai.action.action; - session.beckn_transaction.responses[`on_${session.profile.last_action}`] = response.data; - - }) - - it('Should test get_beckn_request_from_text() succesfully for a `select`', async () => { - - ai.action = {action: 'select'}; - const schema = await ai.get_schema_by_action(); - - session.text.push({"role": "user", "content": trip_planning.FIND_HOTEL}); - session.text.push({"role": "assistant", "content": trip_planning.FIND_HOTEL_RESPONSE}) - const response = await ai.get_beckn_request_from_text(trip_planning.SELECT_HOTEL, on_select.context, schema, session); - expect(response.data).to.be.an('object') - expect(response.data.method.toUpperCase()).to.be.eq('POST') - expect(response.data.url).to.contain('select') - expect(response.data.body.context).to.have.property('transaction_id') - expect(response.data.body.context).to.have.property('bpp_id') - expect(response.data.body.context).to.have.property('bpp_uri') - expect(response.data.body.message).to.have.property('order') - expect(response.data.body.message.order).to.have.property('items') - expect(response.data.body.message.order.items[0]).to.have.property('id'); - session.last_action = ai.action.action; - session.beckn_transaction.responses[`on_${session.last_action}`] = response.data; - }) - - it('Should test get_beckn_request_from_text() succesfully for a `init`', async () => { - ai.action = {action: 'init'}; - const schema = await ai.get_schema_by_action(); - - const response = await ai.get_beckn_request_from_text("Lets place the order. My details are : John Doe, john.doe@example.com, 9999999999", on_init.context, schema, session); - expect(response.data).to.be.an('object') - expect(response.data.method.toUpperCase()).to.be.eq('POST') - expect(response.data.url).to.contain('init') - expect(response.data.body.context).to.have.property('transaction_id') - expect(response.data.body.context).to.have.property('bpp_id') - expect(response.data.body.context).to.have.property('bpp_uri') - expect(response.data.body.message).to.have.property('order') - expect(response.data.body.message.order).to.have.property('items') - expect(response.data.body.message.order.items[0]).to.have.property('id') - expect(response.data.body.message.order).to.have.property('billing') - expect(response.data.body.message.order.billing).to.have.property('name') - expect(response.data.body.message.order.billing).to.have.property('email') - expect(response.data.body.message.order.billing).to.have.property('phone'); - session.last_action = ai.action.action; - session.beckn_transaction.responses[`on_${session.last_action}`] = response.data - }); - - - it('Should test get_beckn_request_from_text() succesfully for a `init` if billing details shared earlier', async () => { - session.profile = { - ...session.profile, - "name": "John Doe", - "email": "john.doe@example.com", - "phone": "9999999999" +describe(`Test cases perform_beckn_transaction()`, ()=> { + it('Should test performing a beckn action succesfully', async () => { + const input = { + action: 'search', + instruction: 'find hotel near Yellowstone National Park on April 12th for 2 days' } - ai.action = {action: 'init'}; - const schema = await ai.get_schema_by_action(); - - const response = await ai.get_beckn_request_from_text("Lets place the order", on_init.context, schema, session); - expect(response.data.body.message.order.billing).to.have.property('name') - expect(response.data.body.message.order.billing.name).to.eq(session.profile.name); - expect(response.data.body.message.order.billing).to.have.property('email') - expect(response.data.body.message.order.billing.email).to.eq(session.profile.email); - expect(response.data.body.message.order.billing).to.have.property('phone') - expect(response.data.body.message.order.billing.phone).to.eq(session.profile.phone); - session.last_action = ai.action.action; - session.beckn_transaction.responses[`on_${session.last_action}`] = response.data - }); - - it('Should test get_beckn_request_from_text() succesfully for a `confirm`', async () => { - - ai.action = {action: 'confirm'}; - const schema = await ai.get_schema_by_action(); - - const response = await ai.get_beckn_request_from_text("Lets confirm the order!", on_confirm.context, schema, session); - expect(response.data).to.be.an('object') - expect(response.data.method.toUpperCase()).to.be.eq('POST') - expect(response.data.url).to.contain('confirm') - expect(response.data.body.context).to.have.property('transaction_id') - expect(response.data.body.context).to.have.property('bpp_id') - expect(response.data.body.context).to.have.property('bpp_uri') - expect(response.data.body.message).to.have.property('order') - expect(response.data.body.message.order).to.have.property('items') - expect(response.data.body.message.order.items[0]).to.have.property('id') - session.last_action = ai.action.action; - session.beckn_transaction.responses[`on_${session.last_action}`] = response.data - }); -}); - - -describe.skip('Test cases for services/ai/format_response()', () => { - it('Should test format_response() and throw response with success false for empty object', async () => { - const response = await ai.format_response({}) - expect(response.status).to.equal(false) - }) - it('Should test format_response() return some message with success true', async () => { - const context = [ - {role: 'user', content: 'I want to search for some ev chargers'} - ] - const response = await ai.format_response(on_search, context) - expect(response.status).to.equal(true) - }) -}) - -describe.skip('Test cases for services/ai/get_beckn_message_from_text()', () => { - it('Should return the correct message for a search by name', async () => { - let instruction = "I'm looking for some raincoats"; - let response = await ai.get_beckn_message_from_text(instruction) + const response = await ai.perform_beckn_transaction(input); expect(response).to.be.an('object'); - expect(response).to.have.property('intent'); - expect(response.intent.item).to.have.property('descriptor'); - expect(response.intent.item.descriptor).to.have.property('name'); + expect(response.status).to.be.true; + expect(response.data.length).to.be.gt(0); }) - it.skip('Should return the correct message for a search by location', async () => { - let instruction = "I'm looking for some ev chargers. Lat: 30.876877, long: 73.868969"; - let response = await ai.get_beckn_message_from_text(instruction, [], 'uei:charging') + it('Should test performing a beckn action with invalid action', async () => { + const input = { + action: 'bla', + instruction: 'find hotel near Yellowstone National Park on April 12th for 2 days' + } + const response = await ai.perform_beckn_transaction(input); expect(response).to.be.an('object'); - expect(response).to.have.property('intent'); - expect(response.intent).to.have.property('fulfillment'); - expect(response.intent.fulfillment.stops[0].location).to.have.property('gps'); + expect(response.status).to.be.false; }) +}) - it('Should return the correct message for a search by location along with tags', async () => { - let instruction = "I'm looking for some ev chargers near my location 30.876877, 73.868969. I'm using a 4-wheeler with CCS connector."; - let response = await ai.get_beckn_message_from_text(instruction, [], 'uei:charging') +describe(`Test cases for get_context_by_action()`, ()=> { + it('Should return context for a valid action', async () => { + const action = 'search'; + const instruction = 'find hotel near Yellowstone National Park on April 12th for 2 days' + const response = await ai.get_context_by_action(action, instruction); expect(response).to.be.an('object'); - expect(response).to.have.property('intent'); - expect(response.intent.item).to.have.property('tags').that.is.an('array').that.is.not.empty; - expect(response.intent).to.have.property('fulfillment'); - expect(response.intent.fulfillment.stops[0].location).to.have.property('gps'); - }) + expect(response.action).to.be.eq(action); + expect(response.domain).to.be.eq('hospitality'); + }) +}) - it('Should return the correct message for a search by location along with tags and fulfillment timings', async () => { - let instruction = "I'm looking for hotels, pet-friendly near yellowstone, with ev-chargin facility. Preferrably campsite. I'm planning to stay for 2 days starting 12th April."; - let response = await ai.get_beckn_message_from_text(instruction, [], 'hospitality') +describe(`Test cases for get_message_by_action()`, ()=> { + it('Should return message for a valid action', async () => { + const action = 'search'; + const instruction = 'find hotel near Yellowstone National Park on April 12th for 2 days' + const response = await ai.get_message_by_action(action, instruction); expect(response).to.be.an('object'); expect(response).to.have.property('intent'); - expect(response.intent.item).to.have.property('tags').that.is.an('array').that.is.not.empty; - expect(response.intent).to.have.property('fulfillment'); - }) -}) - - -describe.skip('Test cases for get_profile_from_text', () => { - it('Should return an object with billing details if billing details shared', async ()=> { - const response = await ai.get_profile_from_text('John Doe, 9999999999, john.doe@example.com'); - expect(response.status).to.be.true; - expect(response.data).to.have.property('name'); - expect(response.data.name).to.eq('John Doe'); - expect(response.data).to.have.property('phone'); - expect(response.data.phone).to.eq('9999999999'); - expect(response.data).to.have.property('email'); - expect(response.data.email).to.eq('john.doe@example.com'); - }) - - it('Should return nothing if no profile information available', async ()=> { - const response = await ai.get_profile_from_text('Yes please'); - expect(response.status).to.be.true; - expect(response.data).to.be.empty; + }) }) diff --git a/tests/unit/services/maps.test.js b/tests/unit/services/maps.test.js index f4a7cd9..6cb888e 100644 --- a/tests/unit/services/maps.test.js +++ b/tests/unit/services/maps.test.js @@ -37,7 +37,8 @@ describe('Should test the map service', () => { const destination = '37.411991,-122.079414'; const point = [37.422391, -122.084845]; - let routes = await mapService.getRoutes({source: source, destination: destination}); + await mapService.getRoutes({source: source, destination: destination}); + let routes = mapService.session.routes; const status = await mapService.checkGpsOnPolygon(point, routes[0].overview_polyline.points); expect(status).to.be.true; From d6b51b7fe561ad20f2a66387c0c69e00dad3018c Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Wed, 17 Apr 2024 00:48:37 +0530 Subject: [PATCH 10/11] Removed unsed data files under tests --- tests/data/api_responses/on_confirm.json | 273 ----- tests/data/api_responses/on_init.json | 347 ------ tests/data/api_responses/on_search.json | 1091 ----------------- .../api_responses/on_search_compressed.json | 92 -- tests/data/api_responses/on_select.json | 333 ----- tests/data/chats/trip_planning.json | 13 - tests/data/sessions/hotel.json | 84 -- 7 files changed, 2233 deletions(-) delete mode 100644 tests/data/api_responses/on_confirm.json delete mode 100644 tests/data/api_responses/on_init.json delete mode 100644 tests/data/api_responses/on_search.json delete mode 100644 tests/data/api_responses/on_search_compressed.json delete mode 100644 tests/data/api_responses/on_select.json delete mode 100644 tests/data/chats/trip_planning.json delete mode 100644 tests/data/sessions/hotel.json diff --git a/tests/data/api_responses/on_confirm.json b/tests/data/api_responses/on_confirm.json deleted file mode 100644 index 3c441ad..0000000 --- a/tests/data/api_responses/on_confirm.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "context": { - "ttl": "PT10M", - "action": "confirm", - "timestamp": "2024-03-31T08:23:16.219Z", - "message_id": "255de7b0-3a67-4340-bfd6-c9c876733dd8", - "transaction_id": "a9aaecca-10b7-4d19-b640-b047a7c62196", - "domain": "uei:charging", - "version": "1.1.0", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "location": { - "country": { - "code": "DE" - } - }, - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "https://mit-ps-energy.becknprotocol.io", - "base_url": "https://mit-ps-bap-client.becknprotocol.io" - }, - "responses": [ - { - "context": { - "domain": "uei:charging", - "action": "on_confirm", - "version": "1.1.0", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "country": "IND", - "city": "std:080", - "location": { - "country": { - "code": "DE" - } - }, - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "transaction_id": "a9aaecca-10b7-4d19-b640-b047a7c62196", - "message_id": "255de7b0-3a67-4340-bfd6-c9c876733dd8", - "ttl": "PT10M", - "timestamp": "2024-03-31T08:23:21.474Z" - }, - "message": { - "order": { - "id": "25", - "provider": { - "id": "1", - "descriptor": { - "name": "ChargeZone.in", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "long_desc": "The company offers comprehensive services that include the installation, maintenance, and smart management of EV charging infrastructure. Aimed at empowering eco-conscious drivers and businesses, Chargezone is dedicated to providing reliable and sustainable charging services. The company's approach integrates various aspects such as parking, energy supply, and route information to simplify the EV charging experience for both individual consumers and businesses, offering \"Energy as a Service\" and \"Battery as a Service\" modelsโ€‹", - "additional_desc": { - "url": "www.chargezone.com" - }, - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "1", - "descriptor": { - "name": "energy" - } - }, - { - "id": "2", - "descriptor": { - "name": "Green Energy" - } - }, - { - "id": "3", - "descriptor": { - "name": "Pulse Energy" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations" - } - } - ], - "rating": "4.8", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "locations": [ - { - "id": "4", - "gps": "2.9716,77.5946", - "address": "22, 12, Vittal Mallya Rd, KG Halli, D' Souza Layout, Ashok Nagar, Bengaluru, Karnataka 560001, India", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560001" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true - }, - "items": [ - { - "id": "1", - "descriptor": { - "name": "pe-charging-01", - "code": "pec", - "short_desc": "Energy charging station", - "long_desc": "Energy charging station", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1000, - "measure": { - "value": "1000", - "unit": "kWh" - } - } - }, - "fulfillment_ids": [ - "1" - ] - } - ], - "quote": { - "price": { - "value": "8", - "currency": "INR" - }, - "breakup": [ - { - "title": "Estimated units consumed", - "price": { - "currency": "INR", - "value": "4" - }, - "item": { - "id": "1" - } - }, - { - "title": "Free car wash", - "price": { - "currency": "INR", - "value": "0" - }, - "item": { - "id": "1" - } - } - ] - }, - "billing": { - "name": "Industry buyer", - "address": "B005 aspire heights, Jurong East, SGP, 680230", - "state": { - "name": "Jurong East" - }, - "city": { - "name": "Jurong East" - }, - "email": "nobody@nomail.com", - "phone": "9886098860" - }, - "fulfillments": [ - { - "id": "1", - "customer": { - "contact": { - "email": "fox.judie61234@abc.org", - "phone": "+91-9999999999" - }, - "person": { - "name": "Judie Fox6" - } - }, - "tracking": false - } - ], - "payments": [ - { - "collected_by": "BPP", - "params": { - "bank_account_name": "Unified_BPP_Bank", - "bank_account": "1234002341", - "bank_code": "INB0004321", - "price": "8", - "currency": "INR" - }, - "status": "PAID", - "type": "PRE-ORDER" - } - ], - "type": "DEFAULT" - } - } - } - ] -} \ No newline at end of file diff --git a/tests/data/api_responses/on_init.json b/tests/data/api_responses/on_init.json deleted file mode 100644 index 14fb88c..0000000 --- a/tests/data/api_responses/on_init.json +++ /dev/null @@ -1,347 +0,0 @@ -{ - "context": { - "ttl": "PT10M", - "action": "init", - "timestamp": "2024-03-26T11:18:24.112Z", - "message_id": "523a77f6-5afd-4a4a-a047-d70c0f48129f", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "domain": "uei:charging", - "version": "1.1.0", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "base_url": "https://mit-ps-bap-client.becknprotocol.io" - }, - "responses": [ - { - "context": { - "domain": "uei:charging", - "action": "on_init", - "version": "1.1.0", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "country": "IND", - "city": "std:080", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "message_id": "523a77f6-5afd-4a4a-a047-d70c0f48129f", - "ttl": "PT10M", - "timestamp": "2024-03-26T11:12:33.010Z" - }, - "message": { - "order": { - "provider": { - "id": "1", - "descriptor": { - "name": "ChargeZone.in", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "long_desc": "The company offers comprehensive services that include the installation, maintenance, and smart management of EV charging infrastructure. Aimed at empowering eco-conscious drivers and businesses, Chargezone is dedicated to providing reliable and sustainable charging services. The company's approach integrates various aspects such as parking, energy supply, and route information to simplify the EV charging experience for both individual consumers and businesses, offering \"Energy as a Service\" and \"Battery as a Service\" modelsโ€‹", - "additional_desc": { - "url": "www.chargezone.com" - }, - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "1", - "descriptor": { - "name": "energy" - } - }, - { - "id": "2", - "descriptor": { - "name": "Green Energy" - } - }, - { - "id": "3", - "descriptor": { - "name": "Pulse Energy" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations" - } - } - ], - "rating": "4.8", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "locations": [ - { - "id": "4", - "gps": "2.9716,77.5946", - "address": "22, 12, Vittal Mallya Rd, KG Halli, D' Souza Layout, Ashok Nagar, Bengaluru, Karnataka 560001, India", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560001" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true - }, - "items": [ - { - "id": "1", - "descriptor": { - "name": "pe-charging-01", - "code": "pec", - "short_desc": "Energy charging station", - "long_desc": "Energy charging station", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1000, - "measure": { - "value": "1000", - "unit": "kWh" - } - } - }, - "fulfillment_ids": [ - "1" - ] - } - ], - "quote": { - "price": { - "value": "8", - "currency": "INR" - }, - "breakup": [ - { - "title": "Estimated units consumed", - "price": { - "currency": "INR", - "value": "4" - }, - "item": { - "id": "1" - } - }, - { - "title": "Free car wash", - "price": { - "currency": "INR", - "value": "0" - }, - "item": { - "id": "1" - } - } - ] - }, - "billing": { - "name": "John Doe", - "email": "john.doe@example.com", - "phone": "9000000001" - }, - "categories": [ - { - "id": "1", - "value": "energy", - "createdAt": "2024-03-17T16:31:12.649Z", - "updatedAt": "2024-03-17T16:31:14.959Z", - "publishedAt": "2024-03-17T16:31:14.952Z", - "category_code": "EV" - }, - { - "id": "2", - "value": "Green Energy", - "createdAt": "2024-03-17T16:31:23.376Z", - "updatedAt": "2024-03-17T16:31:24.769Z", - "publishedAt": "2024-03-17T16:31:24.765Z", - "category_code": "GE" - }, - { - "id": "3", - "value": "Pulse Energy", - "createdAt": "2024-03-17T16:31:32.691Z", - "updatedAt": "2024-03-17T16:31:34.039Z", - "publishedAt": "2024-03-17T16:31:34.035Z", - "category_code": "PE" - }, - { - "id": "12", - "value": "Chargers", - "createdAt": "2024-03-19T13:05:29.765Z", - "updatedAt": "2024-03-19T13:05:30.970Z", - "publishedAt": "2024-03-19T13:05:30.963Z", - "category_code": "chargers" - }, - { - "id": "13", - "value": "EV chargers", - "createdAt": "2024-03-19T14:52:01.556Z", - "updatedAt": "2024-03-19T14:52:02.914Z", - "publishedAt": "2024-03-19T14:52:02.895Z", - "category_code": "ev_chargers" - }, - { - "id": "14", - "value": "EV", - "createdAt": "2024-03-19T14:54:18.530Z", - "updatedAt": "2024-03-19T14:54:19.589Z", - "publishedAt": "2024-03-19T14:54:19.574Z", - "category_code": "ev" - }, - { - "id": "15", - "value": "Charging", - "createdAt": "2024-03-19T14:54:36.936Z", - "updatedAt": "2024-03-19T14:54:38.540Z", - "publishedAt": "2024-03-19T14:54:38.534Z", - "category_code": "charging" - }, - { - "id": "16", - "value": "Charging Stations", - "createdAt": "2024-03-19T14:54:57.694Z", - "updatedAt": "2024-03-19T14:54:58.893Z", - "publishedAt": "2024-03-19T14:54:58.887Z", - "category_code": "charging_stations" - }, - { - "id": "17", - "value": "Electric Vehicle", - "createdAt": "2024-03-19T14:55:25.756Z", - "updatedAt": "2024-03-19T14:55:26.785Z", - "publishedAt": "2024-03-19T14:55:26.779Z", - "category_code": "electric_vehicle" - }, - { - "id": "18", - "value": "Energy", - "createdAt": "2024-03-19T14:55:39.408Z", - "updatedAt": "2024-03-19T14:55:42.898Z", - "publishedAt": "2024-03-19T14:55:42.894Z", - "category_code": "energy" - }, - { - "id": "19", - "value": "Charging ports", - "createdAt": "2024-03-19T14:56:02.059Z", - "updatedAt": "2024-03-19T14:56:14.635Z", - "publishedAt": "2024-03-19T14:56:04.067Z", - "category_code": "charging_ports" - }, - { - "id": "20", - "value": "Ev charging stations", - "createdAt": "2024-03-19T14:56:28.350Z", - "updatedAt": "2024-03-19T14:56:30.212Z", - "publishedAt": "2024-03-19T14:56:30.208Z", - "category_code": "ev_charging_stations" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "payments": [ - { - "collected_by": "BPP", - "params": { - "bank_account_name": "Unified_BPP_Bank", - "bank_account": "1234002341", - "bank_code": "INB0004321", - "price": "8", - "currency": "INR" - }, - "status": "PAID", - "type": "PRE-ORDER" - } - ], - "type": "DEFAULT" - } - } - } - ] -} \ No newline at end of file diff --git a/tests/data/api_responses/on_search.json b/tests/data/api_responses/on_search.json deleted file mode 100644 index 0bc9460..0000000 --- a/tests/data/api_responses/on_search.json +++ /dev/null @@ -1,1091 +0,0 @@ -{ - "context": { - "ttl": "PT10M", - "action": "search", - "timestamp": "2024-03-25T14:36:07.512Z", - "message_id": "89d85dea-5d71-4e63-8e6d-f812c25997da", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "domain": "uei:charging", - "version": "1.1.0", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "base_url": "https://mit-ps-bap-client.becknprotocol.io" - }, - "responses": [ - { - "context": { - "domain": "uei:charging", - "action": "on_search", - "version": "1.1.0", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "country": "IND", - "city": "std:080", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "message_id": "89d85dea-5d71-4e63-8e6d-f812c25997da", - "ttl": "PT10M", - "timestamp": "2024-03-25T14:36:12.698Z" - }, - "message": { - "catalog": { - "descriptor": { - "name": "BPP", - "code": "bpp", - "short_desc": "Unified Strapi BPP" - }, - "providers": [ - { - "id": "1", - "descriptor": { - "name": "ChargeZone.in", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "long_desc": "The company offers comprehensive services that include the installation, maintenance, and smart management of EV charging infrastructure. Aimed at empowering eco-conscious drivers and businesses, Chargezone is dedicated to providing reliable and sustainable charging services. The company's approach integrates various aspects such as parking, energy supply, and route information to simplify the EV charging experience for both individual consumers and businesses, offering \"Energy as a Service\" and \"Battery as a Service\" modelsโ€‹", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "1", - "descriptor": { - "name": "energy", - "code": "EV" - } - }, - { - "id": "2", - "descriptor": { - "name": "Green Energy", - "code": "GE" - } - }, - { - "id": "3", - "descriptor": { - "name": "Pulse Energy", - "code": "PE" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers", - "code": "chargers" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers", - "code": "ev_chargers" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV", - "code": "ev" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging", - "code": "charging" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations", - "code": "charging_stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle", - "code": "electric_vehicle" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy", - "code": "energy" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports", - "code": "charging_ports" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations", - "code": "ev_charging_stations" - } - } - ], - "rating": "4.8", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "locations": [ - { - "id": "4", - "gps": "2.9716,77.5946", - "address": "22, 12, Vittal Mallya Rd, KG Halli, D' Souza Layout, Ashok Nagar, Bengaluru, Karnataka 560001, India", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560001" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true, - "items": [ - { - "id": "1", - "descriptor": { - "name": "pe-charging-01", - "code": "pec", - "short_desc": "Energy charging station", - "long_desc": "Energy charging station", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1000 - } - }, - "fulfillment_ids": [ - "1" - ] - }, - { - "id": "2", - "descriptor": { - "name": "pe-charging-02", - "code": "pec-02", - "short_desc": "The ChargePoint Green Energy Fast Charge Hub on 150 Green Energy Rd offers state-of-the-art, high-speed EV charging with 4 CCS and Type 2 connectors at 150 kW powe", - "long_desc": "The ChargePoint Green Energy Fast Charge Hub on 150 Green Energy Rd offers state-of-the-art, high-speed EV charging with 4 CCS and Type 2 connectors at 150 kW power. Open 24/7 with amenities including free WiFi, restrooms, and a cozy cafรฉ nearby. Rated 4.5 stars for its convenience and reliability.", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU" - } - ] - }, - "price": { - "value": "4", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1500 - } - }, - "category_ids": [ - "1", - "2", - "3" - ], - "tags": [ - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Charger type", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Connector type", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Power Rating", - "display": true - } - ] - } - ] - } - ] - }, - { - "id": "2", - "descriptor": { - "name": "log9.in", - "short_desc": "ElectraCharge is a leading provider of high-speed, reliable EV charging solutions, offering an extensive network of charging stations across urban and key travel corridors.", - "long_desc": "ElectraCharge stands at the forefront of electric vehicle (EV) charging technology, providing an expansive and dependable network of charging stations strategically located in urban centers, shopping malls, and along major highways", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "4", - "descriptor": { - "name": "green tariff", - "code": "green_tariff" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers", - "code": "chargers" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports", - "code": "charging_ports" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging", - "code": "charging" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations", - "code": "charging_stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle", - "code": "electric_vehicle" - } - }, - { - "id": "1", - "descriptor": { - "name": "energy", - "code": "EV" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy", - "code": "energy" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV", - "code": "ev" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers", - "code": "ev_chargers" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations", - "code": "ev_charging_stations" - } - }, - { - "id": "2", - "descriptor": { - "name": "Green Energy", - "code": "GE" - } - } - ], - "rating": "4.8", - "short_desc": "ElectraCharge is a leading provider of high-speed, reliable EV charging solutions, offering an extensive network of charging stations across urban and key travel corridors.", - "locations": [ - { - "id": "2", - "gps": "12.9094,77.5736", - "address": "142, 8th Main Road,JP Nagar 3rd Phase,Bangalore, Karnataka, 560078,\nIndia.", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560078" - } - ], - "fulfillments": [ - { - "id": "3", - "type": "BATTERY-SWAP", - "rating": "4.5", - "rateable": true, - "tracking": false - }, - { - "id": "4", - "type": "MOBILE-BATTERY-SWAP", - "rating": "4.5", - "rateable": true, - "tracking": false - }, - { - "id": "3", - "type": "BATTERY-SWAP", - "rating": "4.5", - "rateable": true, - "tracking": false - }, - { - "id": "4", - "type": "MOBILE-BATTERY-SWAP", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true, - "items": [ - { - "id": "4", - "descriptor": { - "name": "es-charging-01", - "code": "ES1001", - "short_desc": " Fast, eco-friendly battery swap in the heart of the city for sedans and SUVs. Lithium-ion swaps under 5 minutes, 24/7. No membership required!", - "long_desc": "Welcome to Energy Station ES1001, your premier urban battery swap hub located in the bustling city center, designed specifically for sedan and SUV drivers. Our state-of-the-art facility offers rapid lithium-ion battery swaps in under 5 minutes, ensuring you're back on the road in no time. Open round the clock, ES1001 caters to all, with no membership necessary. Enjoy our assisted service as you relax with nearby food and beverage options, all while staying connected with our complimentary WiFi. ", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUsoKK5GMUfPhhLBHHE_c7Yqh6FLx0AIRfxg&usqp=CAU" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1500 - } - }, - "category_ids": [ - "4" - ], - "fulfillment_ids": [ - "3" - ], - "tags": [ - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Battery Health Check", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Under 50 kWh", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Self Service", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Weekends Only", - "display": true - } - ] - } - ] - }, - { - "id": "5", - "descriptor": { - "name": "es-charging-02", - "code": "ES2002", - "short_desc": "ES2002: Highway-side self-service battery swap for electric motorcycles. Solid-state batteries, reservations recommended. Solar and wind-powered, open during business hours.", - "long_desc": "Introducing Energy Station ES2002, strategically located along the highway for electric motorcycle enthusiasts. This self-service station specializes in solid-state battery swaps, offering a more efficient, longer-lasting charge for your journeys. To ensure a seamless experience, we recommend reservations, especially for our members who enjoy exclusive benefits and payment options. ES2002 operates within standard business hours, providing a secure and well-equipped environment. Facilities include restrooms and a comfortable waiting lounge, making it an ideal pit stop", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUsoKK5GMUfPhhLBHHE_c7Yqh6FLx0AIRfxg&usqp=CAU" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 2000 - } - }, - "category_ids": [ - "4" - ], - "fulfillment_ids": [ - "4" - ], - "tags": [ - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Battery Health Check", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Under 50 kWh", - "display": true - } - ] - } - ] - } - ] - }, - { - "id": "3", - "descriptor": { - "name": "Green Energy Fast Charge Hub", - "short_desc": "he ChargePoint Green Energy Fast Charge Hub on 150 Green Energy Rd offers state-of-the-art, high-speed EV charging with 4 CCS and Type 2 connectors at 150 kW power. ", - "long_desc": "The ChargePoint Green Energy Fast Charge Hub on 150 Green Energy Rd offers state-of-the-art, high-speed EV charging with 4 CCS and Type 2 connectors at 150 kW power. Open 24/7 with amenities including free WiFi, restrooms, and a cozy cafรฉ nearby. Rated 4.5 stars for its convenience and reliability. Charges at $0.35 per kWh, accepting credit cards and ChargePoint App payments. Perfect for a quick charge and a break on your journey!", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "2", - "descriptor": { - "name": "Green Energy", - "code": "GE" - } - }, - { - "id": "4", - "descriptor": { - "name": "green tariff", - "code": "green_tariff" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV", - "code": "ev" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers", - "code": "chargers" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging", - "code": "charging" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports", - "code": "charging_ports" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations", - "code": "charging_stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle", - "code": "electric_vehicle" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy", - "code": "energy" - } - }, - { - "id": "1", - "descriptor": { - "name": "energy", - "code": "EV" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers", - "code": "ev_chargers" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations", - "code": "ev_charging_stations" - } - } - ], - "rating": "4.8", - "short_desc": "he ChargePoint Green Energy Fast Charge Hub on 150 Green Energy Rd offers state-of-the-art, high-speed EV charging with 4 CCS and Type 2 connectors at 150 kW power. ", - "locations": [ - { - "id": "5", - "gps": "12.9763,77.5848", - "address": "Shop No-120, 11th Cross, Margosa Rd, near Bank of India, Malleshwaram, Bengaluru, Karnataka 560003, India", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560003" - } - ], - "rateable": true - }, - { - "id": "7", - "descriptor": { - "name": "Yellowstone EcoDrive", - "short_desc": "Rent eco-friendly electric vehicles for exploring Yellowstone Park and join guided eco-tours.", - "long_desc": "Rental of electric vehicles for exploring Yellowstone National Park and surrounding areas, including guided eco-tours.", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "13", - "descriptor": { - "name": "EV chargers", - "code": "ev_chargers" - } - } - ], - "rating": "4", - "short_desc": "Rent eco-friendly electric vehicles for exploring Yellowstone Park and join guided eco-tours.", - "locations": [ - { - "id": "9", - "address": "123 Eco Drive, Yellowstone Village", - "city": { - "name": "Jackson Hole" - }, - "country": { - "name": "USA" - }, - "state": { - "name": "Wyoming" - }, - "area_code": "82412" - } - ], - "rateable": true, - "items": [ - { - "id": "13", - "descriptor": { - "name": "Guided Eco-Tours", - "code": "Yellowstone_Eco", - "short_desc": "Guided Tours", - "long_desc": "Yellowstone EcoDrive's guided tours, explore the natural wonders of Yellowstone National Park in electric vehicles while minimizing environmental impact.", - "images": [ - { - "url": "https://www.pickpik.com/electric-charge-road-sign-sign-hybrid-automobile-electric-129332" - } - ] - }, - "price": { - "value": "50", - "currency": "USD" - }, - "quantity": { - "available": { - "count": 100 - } - }, - "category_ids": [ - "29" - ] - } - ] - }, - { - "id": "9", - "descriptor": { - "name": "GreenWheels EV Rentals", - "short_desc": "Experience Yellowstone sustainably with electric car and SUV rentals, perfect for eco-conscious travelers.", - "long_desc": "Offers a fleet of electric cars and SUVs for rent, ideal for eco-conscious travelers visiting Yellowstone.", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "14", - "descriptor": { - "name": "EV", - "code": "ev" - } - } - ], - "rating": "4", - "short_desc": "Experience Yellowstone sustainably with electric car and SUV rentals, perfect for eco-conscious travelers.", - "locations": [ - { - "id": "9", - "address": "123 Eco Drive, Yellowstone Village", - "city": { - "name": "Jackson Hole" - }, - "country": { - "name": "USA" - }, - "state": { - "name": "Wyoming" - }, - "area_code": "82412" - } - ], - "fulfillments": [ - { - "id": "5", - "type": "BATTERY CHARGING", - "rating": "4.5", - "tracking": false - }, - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true, - "items": [ - { - "id": "14", - "descriptor": { - "name": "Electric Car Rentals", - "code": "Green-Wheels", - "short_desc": "Car Rentals", - "long_desc": "GreenWheels EV Rentals offers electric car rentals, providing eco-conscious travelers visiting Yellowstone National Park with sustainable transportation options to explore the majestic landscapes and wildlife of the region.", - "images": [ - { - "url": "https://www.flickr.com/photos/143789194@N03/46991696195" - } - ] - }, - "price": { - "value": "60", - "currency": "USD" - }, - "quantity": { - "available": { - "count": 110 - } - }, - "category_ids": [ - "29" - ], - "fulfillment_ids": [ - "1" - ] - } - ] - }, - { - "id": "10", - "descriptor": { - "name": "EcoRides Yellowstone", - "short_desc": "Enjoy eco-friendly shuttle services within Yellowstone Park, reducing environmental impact while exploring.", - "long_desc": "Provides electric shuttle services within Yellowstone National Park, reducing carbon footprint and offering convenient transportation for visitors.", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "15", - "descriptor": { - "name": "Charging", - "code": "charging" - } - } - ], - "rating": "4", - "short_desc": "Enjoy eco-friendly shuttle services within Yellowstone Park, reducing environmental impact while exploring.", - "locations": [ - { - "id": "10", - "address": "456 Electric Avenue, Yellowstone Springs", - "city": { - "name": "Jackson Hole" - }, - "country": { - "name": "USA" - }, - "state": { - "name": "Wyoming" - }, - "area_code": "82412" - } - ], - "fulfillments": [ - { - "id": "2", - "type": "CHARGING", - "rating": "4.6", - "rateable": true, - "tracking": false - }, - { - "id": "5", - "type": "BATTERY CHARGING", - "rating": "4.5", - "tracking": false - } - ], - "rateable": true, - "items": [ - { - "id": "15", - "descriptor": { - "name": "Shuttle Service", - "code": "Ecoride-yellow", - "short_desc": "Shuttle Service", - "long_desc": "EcoRides Yellowstone provides eco-friendly shuttle services within Yellowstone National Park, offering convenient and environmentally conscious transportation options for visitors to explore the park's diverse attractions and scenic beauty.", - "images": [ - { - "url": "https://vectorportal.com/vector/electric-car-charging/18385" - } - ] - }, - "price": { - "value": "50", - "currency": "USD" - }, - "quantity": { - "available": { - "count": 150 - } - }, - "category_ids": [ - "31" - ], - "fulfillment_ids": [ - "5" - ] - } - ] - }, - { - "id": "11", - "descriptor": { - "name": "CleanCharge EV Station", - "short_desc": "Conveniently charge your electric vehicle at fast-charging stations near Yellowstone, promoting clean transportation.", - "long_desc": "Operates electric vehicle charging stations equipped with fast-charging capabilities for travelers exploring Yellowstone and nearby areas.", - "additional_desc": { - "url": "www.makerspace.com" - }, - "images": [ - { - "url": "https://makerspace/assembly/makerspace_logo.png", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "15", - "descriptor": { - "name": "Charging", - "code": "charging" - } - } - ], - "rating": "4", - "short_desc": "Conveniently charge your electric vehicle at fast-charging stations near Yellowstone, promoting clean transportation.", - "locations": [ - { - "id": "11", - "address": "789 Greenway Boulevard, Yellowstone Junction", - "city": { - "name": "Jackson Hole" - }, - "country": { - "name": "USA" - }, - "state": { - "name": "Wyoming" - }, - "area_code": "82412" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - }, - { - "id": "6", - "type": "INSTRUCTOR LED TRAINING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true, - "items": [ - { - "id": "6", - "descriptor": { - "name": "ev charging", - "code": "evc", - "short_desc": "Electric Vehicle (EV) charging involves replenishing the battery of an electric vehicle by connecting it to a power source or charging station.", - "long_desc": "Electric Vehicle (EV) charging is a critical component of the electric vehicle ecosystem, providing the necessary infrastructure to ensure that electric vehicles remain a viable and sustainable alternative to traditional internal combustion engine vehicles. EV charging can be categorized into different levels based on charging speed:", - "images": [ - { - "url": "https://greenehergy.in/imaghttps://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS5dkBso2rv_c0RYFQjDoa6A_Xu1sJsnwmtUw&usqp=CAUes/logo.png" - } - ] - }, - "price": { - "value": "0", - "currency": "INR" - }, - "category_ids": [ - "5", - "6" - ], - "tags": [ - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Urban Center", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Lithium Ion", - "display": true - } - ] - }, - { - "display": true, - "descriptor": { - "description": "uei:charging" - }, - "list": [ - { - "value": "Wind Energy Supported", - "display": true - } - ] - } - ] - }, - { - "id": "16", - "descriptor": { - "name": " EV Charging", - "code": "Clean-Charge", - "short_desc": "Charging Stations", - "long_desc": "CleanCharge EV Station operates electric vehicle charging stations near Yellowstone National Park, facilitating clean transportation and sustainable travel by providing fast-charging solutions for electric vehicle owners exploring the natural wonders of the park and its surroundings.", - "images": [ - { - "url": "https://www.istockphoto.com/vector/fuel-the-gun-oil-prices-drop-gm1390673273-447549241" - } - ] - }, - "price": { - "value": "50", - "currency": "USD" - }, - "quantity": { - "available": { - "count": 200 - } - }, - "category_ids": [ - "32" - ], - "fulfillment_ids": [ - "6" - ] - } - ] - } - ] - } - } - } - ] -} \ No newline at end of file diff --git a/tests/data/api_responses/on_search_compressed.json b/tests/data/api_responses/on_search_compressed.json deleted file mode 100644 index f160a68..0000000 --- a/tests/data/api_responses/on_search_compressed.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "context" : { - "ttl": "PT10M", - "action": "search", - "timestamp": "2024-03-25T14:36:07.512Z", - "message_id": "89d85dea-5d71-4e63-8e6d-f812c25997da", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "domain": "uei:charging", - "version": "1.1.0", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "base_url": "https://mit-ps-bap-client.becknprotocol.io" - }, - "responses": { - "providers": [ - { - "id": "1", - "name": "ChargeZone.in", - "items": [ - { - "id": "1", - "name": "pe-charging-01" - }, - { - "id": "2", - "name": "pe-charging-02" - } - ] - }, - { - "id": "2", - "name": "log9.in", - "items": [ - { - "id": "4", - "name": "es-charging-01" - }, - { - "id": "5", - "name": "es-charging-02" - } - ] - }, - { - "id": "7", - "name": "Yellowstone EcoDrive", - "items": [ - { - "id": "13", - "name": "Guided Eco-Tours" - } - ] - }, - { - "id": "9", - "name": "GreenWheels EV Rentals", - "items": [ - { - "id": "14", - "name": "Electric Car Rentals" - } - ] - }, - { - "id": "10", - "name": "EcoRides Yellowstone", - "items": [ - { - "id": "15", - "name": "Shuttle Service" - } - ] - }, - { - "id": "11", - "name": "CleanCharge EV Station", - "items": [ - { - "id": "6", - "name": "ev charging" - }, - { - "id": "16", - "name": "EV Charging" - } - ] - } - ] - } -} \ No newline at end of file diff --git a/tests/data/api_responses/on_select.json b/tests/data/api_responses/on_select.json deleted file mode 100644 index 630d3d1..0000000 --- a/tests/data/api_responses/on_select.json +++ /dev/null @@ -1,333 +0,0 @@ -{ - "context": { - "ttl": "PT10M", - "action": "select", - "timestamp": "2024-03-26T07:51:46.539Z", - "message_id": "e86d2100-49cd-4fea-99d8-abda400c79ae", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "domain": "uei:charging", - "version": "1.1.0", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "base_url": "https://mit-ps-bap-client.becknprotocol.io" - }, - "responses": [ - { - "context": { - "domain": "uei:charging", - "action": "on_select", - "version": "1.1.0", - "bpp_id": "mit-ps-energy.becknprotocol.io", - "bpp_uri": "http://mit-ps-energy.becknprotocol.io", - "country": "IND", - "city": "std:080", - "bap_id": "mit-ps-bap.becknprotocol.io", - "bap_uri": "https://mit-ps-bap.becknprotocol.io", - "transaction_id": "99847583-fd40-4c0f-b682-f53ea3061a74", - "message_id": "e86d2100-49cd-4fea-99d8-abda400c79ae", - "ttl": "PT10M", - "timestamp": "2024-03-26T07:51:51.596Z" - }, - "message": { - "order": { - "provider": { - "id": "1", - "descriptor": { - "name": "ChargeZone.in", - "short_desc": "Chargezone is recognized as a leading provider of electric vehicle (EV) charging solutions in India. ", - "long_desc": "The company offers comprehensive services that include the installation, maintenance, and smart management of EV charging infrastructure. Aimed at empowering eco-conscious drivers and businesses, Chargezone is dedicated to providing reliable and sustainable charging services. The company's approach integrates various aspects such as parking, energy supply, and route information to simplify the EV charging experience for both individual consumers and businesses, offering \"Energy as a Service\" and \"Battery as a Service\" modelsโ€‹", - "additional_desc": { - "url": "www.chargezone.com" - }, - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "categories": [ - { - "id": "1", - "descriptor": { - "name": "energy" - } - }, - { - "id": "2", - "descriptor": { - "name": "Green Energy" - } - }, - { - "id": "3", - "descriptor": { - "name": "Pulse Energy" - } - }, - { - "id": "12", - "descriptor": { - "name": "Chargers" - } - }, - { - "id": "13", - "descriptor": { - "name": "EV chargers" - } - }, - { - "id": "14", - "descriptor": { - "name": "EV" - } - }, - { - "id": "15", - "descriptor": { - "name": "Charging" - } - }, - { - "id": "16", - "descriptor": { - "name": "Charging Stations" - } - }, - { - "id": "17", - "descriptor": { - "name": "Electric Vehicle" - } - }, - { - "id": "18", - "descriptor": { - "name": "Energy" - } - }, - { - "id": "19", - "descriptor": { - "name": "Charging ports" - } - }, - { - "id": "20", - "descriptor": { - "name": "Ev charging stations" - } - } - ], - "rating": "4.8", - "locations": [ - { - "id": "4", - "gps": "2.9716,77.5946", - "address": "22, 12, Vittal Mallya Rd, KG Halli, D' Souza Layout, Ashok Nagar, Bengaluru, Karnataka 560001, India", - "city": { - "name": "Bengaluru" - }, - "country": { - "name": "India" - }, - "state": { - "name": "Karnataka" - }, - "area_code": "560001" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "rateable": true - }, - "items": [ - { - "id": "1", - "descriptor": { - "name": "pe-charging-01", - "code": "pec", - "short_desc": "Energy charging station", - "long_desc": "Energy charging station", - "images": [ - { - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbJ3iLFYAbgJ4LWvKVsbSd-tCNWfIbYRx3lw&usqp=CAU", - "size_type": "sm" - } - ] - }, - "price": { - "value": "8", - "currency": "INR" - }, - "quantity": { - "available": { - "count": 1000, - "measure": { - "value": "1000", - "unit": "kWh" - } - } - }, - "fulfillment_ids": [ - "1" - ], - "xinput": { - "form": { - "url": "https://mit-bpp-energy.becknprotocol.io/beckn-bpp-adapter/x-input/form?form_id=itemDetailsForm", - "mime_type": "text/html" - } - } - } - ], - "quote": { - "price": { - "value": "8", - "currency": "INR" - }, - "breakup": [ - { - "title": "Estimated units consumed", - "price": { - "currency": "INR", - "value": "4" - }, - "item": { - "id": "1" - } - }, - { - "title": "Free car wash", - "price": { - "currency": "INR", - "value": "0" - }, - "item": { - "id": "1" - } - } - ] - }, - "categories": [ - { - "id": "1", - "value": "energy", - "createdAt": "2024-03-17T16:31:12.649Z", - "updatedAt": "2024-03-17T16:31:14.959Z", - "publishedAt": "2024-03-17T16:31:14.952Z", - "category_code": "EV" - }, - { - "id": "2", - "value": "Green Energy", - "createdAt": "2024-03-17T16:31:23.376Z", - "updatedAt": "2024-03-17T16:31:24.769Z", - "publishedAt": "2024-03-17T16:31:24.765Z", - "category_code": "GE" - }, - { - "id": "3", - "value": "Pulse Energy", - "createdAt": "2024-03-17T16:31:32.691Z", - "updatedAt": "2024-03-17T16:31:34.039Z", - "publishedAt": "2024-03-17T16:31:34.035Z", - "category_code": "PE" - }, - { - "id": "12", - "value": "Chargers", - "createdAt": "2024-03-19T13:05:29.765Z", - "updatedAt": "2024-03-19T13:05:30.970Z", - "publishedAt": "2024-03-19T13:05:30.963Z", - "category_code": "chargers" - }, - { - "id": "13", - "value": "EV chargers", - "createdAt": "2024-03-19T14:52:01.556Z", - "updatedAt": "2024-03-19T14:52:02.914Z", - "publishedAt": "2024-03-19T14:52:02.895Z", - "category_code": "ev_chargers" - }, - { - "id": "14", - "value": "EV", - "createdAt": "2024-03-19T14:54:18.530Z", - "updatedAt": "2024-03-19T14:54:19.589Z", - "publishedAt": "2024-03-19T14:54:19.574Z", - "category_code": "ev" - }, - { - "id": "15", - "value": "Charging", - "createdAt": "2024-03-19T14:54:36.936Z", - "updatedAt": "2024-03-19T14:54:38.540Z", - "publishedAt": "2024-03-19T14:54:38.534Z", - "category_code": "charging" - }, - { - "id": "16", - "value": "Charging Stations", - "createdAt": "2024-03-19T14:54:57.694Z", - "updatedAt": "2024-03-19T14:54:58.893Z", - "publishedAt": "2024-03-19T14:54:58.887Z", - "category_code": "charging_stations" - }, - { - "id": "17", - "value": "Electric Vehicle", - "createdAt": "2024-03-19T14:55:25.756Z", - "updatedAt": "2024-03-19T14:55:26.785Z", - "publishedAt": "2024-03-19T14:55:26.779Z", - "category_code": "electric_vehicle" - }, - { - "id": "18", - "value": "Energy", - "createdAt": "2024-03-19T14:55:39.408Z", - "updatedAt": "2024-03-19T14:55:42.898Z", - "publishedAt": "2024-03-19T14:55:42.894Z", - "category_code": "energy" - }, - { - "id": "19", - "value": "Charging ports", - "createdAt": "2024-03-19T14:56:02.059Z", - "updatedAt": "2024-03-19T14:56:14.635Z", - "publishedAt": "2024-03-19T14:56:04.067Z", - "category_code": "charging_ports" - }, - { - "id": "20", - "value": "Ev charging stations", - "createdAt": "2024-03-19T14:56:28.350Z", - "updatedAt": "2024-03-19T14:56:30.212Z", - "publishedAt": "2024-03-19T14:56:30.208Z", - "category_code": "ev_charging_stations" - } - ], - "fulfillments": [ - { - "id": "1", - "type": "CHARGING", - "rating": "4.5", - "rateable": true, - "tracking": false - } - ], - "type": "DEFAULT" - } - } - } - ] -} \ No newline at end of file diff --git a/tests/data/chats/trip_planning.json b/tests/data/chats/trip_planning.json deleted file mode 100644 index 7653730..0000000 --- a/tests/data/chats/trip_planning.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "TRIP_QUERY": "Iโ€™m planning a trip from denver to yellostone national park", - "TRIP_QUERY_RESPONSE": "Great! I can help you plan your trip from Denver to Yellowstone National Park. Before we start, could you please provide me with the following details to help personalize your trip better?1. Travel dates: When are you planning to go on this trip?2. Number of travelers: How many people will be going on this trip?3. Accommodation preferences: What type of accommodation are you looking for (e.g., hotels, camping, cabins)?4. Transportation preferences: How do you prefer to travel from Denver to Yellowstone National Park (e.g., car rental, bus, train)?5. Dietary preferences: Do you have any specific dietary requirements or preferences?6. Interests and activities: Are there any specific activities or attractions you would like to include on your trip?7 Once I have this information, I can provide you with a tailored itinerary for your trip from Denver to Yellowstone National Park.", - "TRIP_DETAILS": "Sure, I'm planning the trip on APril 12th, I'm travelling with my family of 4. I also have a shihtzu dog. I have an EV vehicle, want to stay 1 day at the national park. I am a vegan. I want to stay near Casper 1 day to take a break.", - "TRIP_DETAILS_RESPONSE" : "Great! Thank you for providing those details. Here's a suggested itinerary for your trip:Day 1: April 12th- Depart from your home with your family of 4 and your Shih Tzu dog in your EV vehicle- Travel to the national park and stay overnight at a pet-friendly vegan accommodation near the parkDay 2: April 13th- Spend the day exploring the national park with your family and dog- Stay overnight at the same accommodation near the parkDay 3: April 14th- Check out from the national park accommodation- Travel to Casper and stay overnight at a vegan-friendly accommodation in the area to take a break and explore the surroundingsPlease let me know if you have any specific preferences for accommodations, activities or any other special requests. It's always good to double-check the availability and policies for pet-friendly and vegan options. Enjoy your trip!", - "BOOKINGS_QUERY": "Can you list down what all bookings should be made?", - "FIND_HOTEL": "Lets find hotels near Yellowstone national park", - "FIND_HOTEL_RESPONSE" : "๐ŸŒฒ Search Results ๐ŸŒฒ1. ๐ŸŽซ Yellowstone national park - Standard Entrance Pass๐Ÿ“ Location: 356 Forest Road, Yellowstone National Park, Cody, Wyoming, USA๐Ÿ’ฐ Price: $5โญ Rating: 4.8โ„น๏ธ Description: Tickets for people above the age of 122. ๐ŸŽซ Hiking Pass - Adult๐Ÿ“ Location: 356 Forest Road, Yellowstone National Park, Cody, Wyoming, USA๐Ÿ’ฐ Price: $5โญ Rating: 4.8โ„น๏ธ Description: Tickets for people below the age of 123. ๐ŸŽซ Hiking Pass - Elders๐Ÿ“ Location: 356 Forest Road, Yellowstone National Park, Cody, Wyoming, USA๐Ÿ’ฐ Price: $5โญ Rating: 4.8โ„น๏ธ Description: Tickets for people above the age of 704. ๐Ÿ›๏ธ National Mueseum๐Ÿ“ Location: 356 Forest Road, Yellowstone National Park, Cody, Wyoming, USA๐Ÿ’ฐ Price: $200โญ Rating: 4.8โ„น๏ธ Description: National Museum for kids. Please select an item to proceed or type 'init' to share your billing details.", - "SELECT_HOTEL": "Lets select the first one.", - "SELECT_HOTEL_RESPONSE": "You have selected the first one, Please provide your billing and shipping details to proceed to the next step.", - "INIT_HOTEL": "Lets place the order. My details are John Doe, 9999999999, test@example.com", - "CONFIRM_HOTEL": "Please confirm the order!" -} \ No newline at end of file diff --git a/tests/data/sessions/hotel.json b/tests/data/sessions/hotel.json deleted file mode 100644 index 28e0f34..0000000 --- a/tests/data/sessions/hotel.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "data":{ - "actions":{ - "formatted":[ - { - "content":"lets find some hotels near Yellowstone National Park", - "role":"user" - }, - { - "content":"๐Ÿจ **Wilderness Lodge & Pet Haven** ๐Ÿ“ Location: 123 Forest Road, Yellowstone Village, Jackson Hole, Wyoming, USA ๐ŸŒŸ Rating: 4.8 ๐Ÿ’ฐ Price: $50 per night ๐Ÿจ **Paws and Peaks Resort** ๐Ÿ“ Location: 456 Canyon Avenue, Yellowstone Springs, Cody, Wyoming, USA ๐ŸŒŸ Rating: 4.7 ๐Ÿ’ฐ Price: $180 per night ๐Ÿจ **Yellowstone Trails Inn** ๐Ÿ“ Location: 123 Forest Road, Yellowstone Village, Jackson Hole, Wyoming, USA ๐ŸŒŸ Rating: 4.5 ๐Ÿ’ฐ Price: $210 per night Please select a hotel for more details.", - "role":"assistant" - }, - { - "content":"I liked the first one", - "role":"user" - }, - { - "content":"๐Ÿจ *Wilderness Lodge & Pet Haven* ๐ŸŒŸ Rating: 4.8 ๐Ÿ“ Location: 123 Forest Road, Yellowstone Village, Jackson Hole, Wyoming, USA ๐Ÿ’ฐ Price: $50 per night ๐Ÿ› *Room Details:* - Type: Deluxe Room - Description: Queen Size Beds with AC - Pet-Friendly accomodation for your hike plans ๐Ÿ“Œ *Additional Information:* - Website: [www.wildernesslodgepethaven.com](www.wildernesslodgepethaven.com) Would you like to proceed with this booking?", - "role":"assistant" - }, - { - "content":"Yes please", - "role":"user" - }, - { - "content":"Great choice! Please share your billing details to initiate the order.", - "role":"assistant" - }, - { - "content":"lets initiate the order, My details are : John Doe, 9999999999, test@example.com", - "role":"user" - }, - { - "content":"Great choice! Your order has been initiated for a Deluxe Room at Wilderness Lodge & Pet Haven. Please confirm your order to proceed.", - "role":"assistant" - }, - { - "content":"Lets confirm", - "role":"user" - }, - { - "content":"Your order has been confirmed. Order ID: 4. Wilderness Lodge & Pet Haven has been booked for you. Please check your email for further details.", - "role":"assistant" - } - ] - }, - "sessionId":"whatsapp:+919986949245", - "text":[ - { - "content":"Hi, I want to plan a trip from Denver to Yellowstone national park", - "role":"user" - }, - { - "content":"Great! I'd be happy to help you plan your trip to Yellowstone National Park from Denver. Can you please provide me with some more details to help me tailor the plan to your preferences? 1. How many people will be traveling with you? 2. What dates are you planning to travel? 3. Do you have any specific accommodation preferences (e.g. hotel, camping, Airbnb)? 4. Are there any dietary restrictions or preferences that need to be considered? 5. Do you have any specific interests or activities you would like to do at Yellowstone National Park? 6. Are there any special requests or requirements that you have for this trip?", - "role":"assistant" - }, - { - "content":"Sure, I'm planning the trip on April 12th, I'm travelling with my family of 4. I also have a shitzu dog. I have an EV vehicle, want to stay 1 day at the national park. I am a vegan. I want to stay near Casper 1 day to take a break.", - "role":"user" - }, - { - "content":"Thank you for providing the details. Here's a suggested itinerary for your trip from Denver to Yellowstone National Park with a stop in Casper: Day 1 - Denver to Casper: - Start your journey from Denver in your EV vehicle. - Drive to Casper, Wyoming. It's about a 4-hour drive from Denver. - Check-in to a pet-friendly accommodation in Casper. - Enjoy your stay in Casper and explore the local attractions. Day 2 - Casper to Yellowstone: - Leave Casper and head towards Yellowstone National Park. It's about a 7-hour drive from Casper to the park. - Check-in to a pet-friendly accommodation near the park. There are pet-friendly lodgings available in the park. - Spend the day exploring Yellowstone National Park. There are vegan options available at the park restaurants. - Enjoy your stay at Yellowstone National Park. Day 3 - Return to Denver: - Head back to Denver from Yellowstone National Park. It's about a 9-hour drive from the park to Denver. - Arrive back in Denver and conclude your trip. Please let me know if you have any specific preferences or requirements that you would like to include in the itinerary.", - "role":"assistant" - }, - { - "content":"Can you find some hotels near Casper?", - "role":"user" - }, - { - "content":"I found some pet-friendly hotels near Casper, Wyoming that you might consider for your stay: 1. Best Western PLUS Casper Inn & Suites Address: 651 Granite Peak Dr, Casper, Wyoming Phone: (307) 266-2400 Website: https://www.bestwestern.com/en_US/book/hotels-in-casper/best-western-plus-casper-inn-suites/propertyCode.51033.html 2. Ramkota Hotel & Conference Center Address: 800 N Poplar St, Casper, Wyoming Phone: (307) 266-6000 Website: https://bestwesternwyoming.com/hotels/best-western-plus-ramkota-hotel 3. Courtyard by Marriott Casper Address: 4260 Hospitality Lane, Casper, Wyoming Phone: (307) 473-2600 Website: https://www.marriott.com/hotels/travel/cprcy-courtyard-casper/ These hotels offer pet-friendly accommodations in Casper and are conveniently located for your stay during your trip. You can contact them directly or visit their websites to check availability and make a reservation. Let me know if you need more options or further assistance.", - "role":"assistant" - }, - { - "content":"Iโ€™m looking for some hotels near Caspe", - "role":"user" - }, - { - "content":"I'm sorry for the oversight. Here are some pet-friendly hotels near Casper, Wyoming, that you can consider for your stay: 1. Comfort Inn Evansville - Casper Address: 265 American Rd, Evansville, Wyoming Phone: (307) 234-2770 Website: https://www.choicehotels.com/wyoming/evansville/comfort-inn-hotels/wy223 2. Baymont by Wyndham Casper East Address: 480 Lathrop Rd, Evansville, Wyoming Phone: (307) 234-4900 Website: https://www.wyndhamhotels.com/baymont/casper-wyoming/baymont-inn-and-suites-casper-east/overview 3. MainStay Suites Casper Address: 551 Granite Peak Dr, Casper, Wyoming Phone: (307) 265-3400 Website: https://www.choicehotels.com/wyoming/casper/mainstay-hotels/wy159 These hotels offer pet-friendly accommodations and are located near Casper, Wyoming. You can reach out to them directly or visit their websites for more information and to make a reservation for your stay. Let me know if you need further assistance or more options.", - "role":"assistant" - } - ] - }, - "status":true -} \ No newline at end of file From fa71670deb45d1226688a7060de1d2e8cdeb0f30 Mon Sep 17 00:00:00 2001 From: Mayur Virendra Date: Wed, 17 Apr 2024 13:58:26 +0530 Subject: [PATCH 11/11] made fixes for failing test case --- services/AI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/AI.js b/services/AI.js index 10a48b8..59c9073 100644 --- a/services/AI.js +++ b/services/AI.js @@ -323,7 +323,7 @@ class AI { async _cleanup_beckn_message(action, message){ // cleanup polygon - if(action=='search' && message.intent.fulfillment.stops){ + if(action=='search' && message?.intent?.fulfillment?.stops){ for(let stop of message.intent.fulfillment.stops){ if(stop.location?.polygon){ delete stop.location?.gps;