diff --git a/config/openai.json b/config/openai.json
index 37a5879..9d0d350 100644
--- a/config/openai.json
+++ b/config/openai.json
@@ -1,11 +1,9 @@
{
"SUPPORTED_ACTIONS": [
- { "action": "search", "description": "Perform a search for a service or product. If a service or product is not specified, its not a search. Listing all bookings is not a search." },
- { "action": "select", "description": "If the user likes or selects any item, this action should be used." },
- { "action": "init", "description": "If the user wants to place an order after search and select and has shared the billing details." },
- { "action": "confirm", "description": "Confirm an order. This action gets called when users confirms an order." },
- { "action": "clear_chat", "description": "If the user wants to clear the session or restart session or chat." },
- { "action": "clear_all", "description": "If the user wants to clear the complete session or the profile." }
+ { "action": "search", "description": "If the user clearly indicates to perform a search for a specific product. Sample instructions : 'find a hotel', 'find an ev charger', 'find tickets'" },
+ { "action": "select", "description": "If the user likes or selects any item, this action should be used. This action can only be called if a search has been called before." },
+ { "action": "init", "description": "If the user wants to place an order after search and select and has shared the billing details. This action can only be called if a select has been called before." },
+ { "action": "confirm", "description": "Confirm an order. This action gets called when users confirms an order. This action can only be called if an init has been called before." }
],
"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 and return the extracted details in the following JSON structure : \n\n {'url':'', 'method':'', 'body':''}'"},
diff --git a/config/registry.json b/config/registry.json
index f62fd96..800e82c 100644
--- a/config/registry.json
+++ b/config/registry.json
@@ -8,7 +8,7 @@
"dhp:consultation:0.1.0",
"tourism"
],
- "description": "This network supports multiple domains e.g. uei:charging for ev chargers, retail:1.1.0 for retail stores including grocceries, rain wear, rain cpats, umbrellas and pet supplies, hospitality for hotels/stays/accomodations, dhp:consultation:0.1.0 for doctors or healthcare, tourism for tickets and tours",
+ "description": "This network supports multiple domains e.g. 'uei:charging' for ev chargers. 'retail:1.1.0' for retail items such as grocceries, rain wear, raincoats, umbrellas and pet supplie. 'hospitality' for hotels/stays/accomodations. 'tourism' for tickets",
"bap_subscriber_id": "mit-ps-bap.becknprotocol.io",
"bap_subscriber_url": "https://mit-ps-bap.becknprotocol.io",
"version": "1.1.0",
@@ -20,8 +20,8 @@
{ "connector-type": { "enum": ["CCS", "CHAdeMo"]}}
],
"rules": [
- "intent is not needed for this domain",
- "search should have fulfillment for this domain."
+ "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."
]
},
"hospitality": {
@@ -32,7 +32,23 @@
],
"rules": [
"search must have two stops for this domain.",
- "Supported stop.type : check-in, check-out"
+ "Supported stop.type : check-in, check-out",
+ "fulfillment stops should not have location for this domain.",
+ "fulfillment.stops[i].time should be an object and contain timestamp"
+ ]
+ },
+ "tourism": {
+ "tags": [],
+ "rules":[
+ "item.tags should not be used in search intent for this domain",
+ "fulfillment should not be used in search intent for this domain"
+ ]
+ },
+ "retail:1.1.0": {
+ "tags": [],
+ "rules":[
+ "item.tags should not be used in search intent for this domain",
+ "fulfillment should not be used in search intent for this domain"
]
}
}
diff --git a/controllers/Bot.js b/controllers/Bot.js
index c520a5f..8c76dd0 100644
--- a/controllers/Bot.js
+++ b/controllers/Bot.js
@@ -2,6 +2,7 @@ import ActionsService from '../services/Actions.js'
import AI from '../services/AI.js'
import DBService from '../services/DBService.js'
import logger from '../utils/logger.js'
+import { v4 as uuidv4 } from 'uuid'
const actionsService = new ActionsService()
const db = new DBService();
@@ -53,7 +54,7 @@ async function process_wa_webhook(req, res) {
logger.info(`Sending formatted response to ${sender}: ${process_response.formatted}`)
if(format!='application/json'){
// res.type('text/xml').send(twiml.toString())
- actionsService.send_message(sender, process_response.formatted)
+ await actionsService.send_message(sender, process_response.formatted)
res.send("Done!")
}
else{
@@ -67,10 +68,10 @@ async function process_wa_webhook(req, res) {
}
/**
- * Function to process any text message received by the bot
- * @param {*} req
- * @param {*} res
- */
+* Function to process any text message received by the bot
+* @param {*} req
+* @param {*} res
+*/
async function process_text(req, res) {
let ai = new AI();
@@ -92,9 +93,11 @@ async function process_text(req, res) {
actions : {
raw: [],
formatted: []
- }
+ },
+ bookings: [],
+ active_transaction: null
}
-
+
// Update lat, long
if(req.body.Latitude && req.body.Longitude){
message+=` lat:${req.body.Latitude} long:${req.body.Longitude}`
@@ -110,7 +113,7 @@ async function process_text(req, res) {
}
try{
-
+
// Get profile
const profileResponse = await ai.get_profile_from_text(message, session.profile);
if(profileResponse.status){
@@ -119,63 +122,97 @@ async function process_text(req, res) {
...profileResponse.data
};
}
-
- // get action
- ai.action = await ai.get_beckn_action_from_text(message, session.actions.formatted);
-
- // Reset actions context if action is search
- if(ai.action?.action === 'search') {
- session.actions = EMPTY_SESSION.actions;
- }
-
- if(ai.action?.action === 'clear_chat'){
- session = {
- ...EMPTY_SESSION,
- profile: session.profile
- };
- response.formatted = 'Session cleared! You can start a new session now.';
- }
- else if(ai.action?.action === 'clear_all'){
- session = EMPTY_SESSION;
- response.formatted = 'Session & profile cleared! You can start a new session now.';
- }
- else if(ai.action=="search"){
- session.actions = EMPTY_SESSION.actions; // clear session actions
- }
- else if(ai.action?.action == null) {
- // get ai response
- response.formatted = await ai.get_ai_response_to_query(message, session.text);
+ 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(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{
- response = await process_action(ai.action, message, session, sender);
- // update actions
- if(ai.action?.action === 'confirm') {
+ // get action
+ ai.action = await ai.get_beckn_action_from_text(message, session.text, session.bookings);
+
+ // Reset actions context if action is search
+ if(ai.action?.action === 'search') {
session.actions = EMPTY_SESSION.actions;
+ session.active_transaction = ai.action.transaction_id || uuidv4();
}
- else if(response.formatted && response.raw){
- session.actions.raw.push({ role: 'user', content: message });
- session.actions.raw.push({ role: 'assistant', content: JSON.stringify(response.raw)});
+
+
+ if(ai.action?.action === 'clear_chat'){
+ session = {
+ ...EMPTY_SESSION,
+ profile: session.profile
+ };
+ response.formatted = 'Session cleared! You can start a new session now.';
+ }
+ else if(ai.action?.action === 'clear_all'){
+ session = EMPTY_SESSION;
+ response.formatted = 'Session & profile cleared! You can start a new session now.';
+ }
+ 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.actions.formatted.push({ role: 'user', content: message });
- session.actions.formatted.push({ role: 'assistant', content: response.formatted });
-
session.text.push({ role: 'user', content: message });
- session.text.push({ role: 'assistant', content: response.formatted });
+ session.text.push({ role: 'assistant', content: response.formatted });
+
}
+ else{
+
+ session.bookings = ai.bookings;
+ response = await process_action(ai.action, message, session, sender, format);
+ ai.bookings = response.bookings;
+
+ // update actions
+ if(ai.action?.action === 'confirm') {
+ session.actions = EMPTY_SESSION.actions;
+ session.text = EMPTY_SESSION.text;
+ }
+ else if(response.formatted && response.raw){
+ session.actions.raw.push({ role: 'user', content: message });
+ session.actions.raw.push({ role: 'assistant', content: JSON.stringify(response.raw)});
+
+ session.actions.formatted.push({ role: 'user', content: message });
+ session.actions.formatted.push({ role: 'assistant', content: response.formatted });
+
+ session.text.push({ role: 'user', content: message });
+ session.text.push({ role: 'assistant', content: response.formatted });
+ }
+ }
+
}
-
+
+ // if(session.bookings && session.bookings.length>0) session.bookings = await ai.get_bookings_status(session.bookings, session.text);
+ logger.info(`\u001b[1;34m Bookings status : ${JSON.stringify(ai.bookings)}\u001b[0m`)
+
// update session
+ session.bookings = ai.bookings;
await db.update_session(sender, session);
// Send response
if(format!='application/json'){
- actionsService.send_message(sender, response.formatted)
+ await actionsService.send_message(sender, response.formatted)
res.send("Done!")
}
else (raw_yn && response.raw) ? res.send(response.raw) : res.send(response.formatted)
@@ -189,35 +226,45 @@ async function process_text(req, res) {
}
/**
- * 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(action, text, session, sender=null){
+* 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(action, text, session, sender=null, format='application/json'){
let ai = new AI();
let response = {
raw: null,
- formatted: null
+ formatted: null,
+ bookings: session.bookings
}
ai.action = action;
+ ai.bookings = session.bookings;
+
+ format!='application/json' && await actionsService.send_message(sender, `_Please wait while we process your request through open networks..._`)
- actionsService.send_message(sender, `_Please wait while we process your request through open networks..._`)
-
// Get schema
const schema = await ai.get_schema_by_action(action.action);
// Get config
- const beckn_context = await ai.get_context_by_instruction(text, session.actions.raw);
+ let beckn_context = await ai.get_context_by_instruction(text, session.actions.raw);
+ beckn_context.transaction_id = session.active_transaction;
// Prepare request
if(schema && beckn_context){
let request=null;
if(ai.action.action==='search'){
- const message = await ai.get_beckn_message_from_text(text, session.text, beckn_context.domain);
+ let search_context = session.text;
+ if(session.profile){
+ search_context=[
+ { role: 'system', content: `User pforile: ${JSON.stringify(session.profile)}`},
+ ...search_context
+ ]
+ }
+ const message = await ai.get_beckn_message_from_text(text, search_context, beckn_context.domain);
request = {
status: true,
data:{
@@ -231,35 +278,53 @@ async function process_action(action, text, session, sender=null){
}
}
else{
- request = await ai.get_beckn_request_from_text(text, session.actions.raw, beckn_context, schema);
+ request = await ai.get_beckn_request_from_text(text, session.actions.raw, beckn_context, schema, session.profile);
}
if(request.status){
// call api
const api_response = await actionsService.call_api(request.data.url, request.data.method, request.data.body, request.data.headers)
- actionsService.send_message(sender, `_Your request is processed, generating a response..._`)
+ format!='application/json' && await actionsService.send_message(sender, `_Your request is processed, generating a response..._`)
if(!api_response.status){
- response.formatted = `Failed to call the API: ${api_response.error}`
+ logger.error(`Failed to call the API: ${api_response.error}`)
+ response.formatted = 'Request could not be processed. Do you want to try again?'
}
else{
+
response.raw = request.data.body.context.action==='search' ? await ai.compress_search_results(api_response.data) : api_response.data
- const formatted_response = await ai.get_text_from_json(
+
+ // update booking status
+ if (ai.action && ai.action.action === 'confirm') {
+ response.bookings = ai.bookings.map(booking => {
+ if (booking.transaction_id === response.raw.context.transaction_id) {
+ booking.booked_yn = 1;
+ }
+ return booking;
+ });
+ logger.info(`Updated bookings: ${JSON.stringify(response.bookings)}`);
+ }
+ ai.bookings = response.bookings;
+
+ const formatted_response = await ai.format_response(
api_response.data,
[...session.actions.formatted, { role: 'user', content: text }],
session.profile
- );
- response.formatted = formatted_response.message;
- }
- }
- else{
- response.formatted = "Could not prepare this request. Can you please try something else?"
+ );
+ response.formatted = formatted_response.message;
+ }
+
+
+
+ }
+ else{
+ response.formatted = "Could not process this request. Can you please try something else?"
+ }
}
+
+ return response;
}
- return response;
-}
-
-export default {
- process_wa_webhook,
- process_text
-}
+ export default {
+ process_wa_webhook,
+ process_text
+ }
diff --git a/schemas/core_1.1.0/confirm.yml b/schemas/core_1.1.0/confirm.yml
index 715970b..4b6db4a 100644
--- a/schemas/core_1.1.0/confirm.yml
+++ b/schemas/core_1.1.0/confirm.yml
@@ -107,23 +107,6 @@ components:
- billing
- fulfillments
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.
- 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
items:
description: The items purchased / availed in this order. This should be based on what was used in the `select` call.
type: array
@@ -146,21 +129,6 @@ components:
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
FulfillmentState:
description: Describes the state of fulfillment
type: object
@@ -268,18 +236,6 @@ components:
name:
description: Name of the billable entity
type: string
- 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
@@ -287,32 +243,6 @@ components:
phone:
description: Phone number of the billable entity
type: string
- tax_id:
- description: ID of the billable entity as recognized by the taxation authority
- type: string
- Address:
- description: Describes a postal address.
- type: string
- 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
- City:
- description: Describes a city
- type: object
- properties:
- name:
- description: Name of the city
- type: string
- code:
- description: City code
- type: string
ItemQuantity:
description: Describes the count or amount of an item
type: object
diff --git a/schemas/core_1.1.0/init.yml b/schemas/core_1.1.0/init.yml
index 40503aa..025c6d3 100644
--- a/schemas/core_1.1.0/init.yml
+++ b/schemas/core_1.1.0/init.yml
@@ -106,23 +106,6 @@ components:
- items
- billing
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.
- 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
billing:
description: The billing details of this order
allOf:
@@ -139,18 +122,6 @@ components:
name:
description: Name of the billable entity
type: string
- 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
@@ -158,32 +129,6 @@ components:
phone:
description: Phone number of the billable entity
type: string
- tax_id:
- description: ID of the billable entity as recognized by the taxation authority
- type: string
- Address:
- description: Describes a postal address.
- type: string
- 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
- City:
- description: Describes a city
- type: object
- properties:
- name:
- description: Name of the city
- type: string
- code:
- description: City code
- type: string
ItemQuantity:
description: Describes the count or amount of an item
type: object
diff --git a/schemas/core_1.1.0/select.yml b/schemas/core_1.1.0/select.yml
index b7bc181..89f86e0 100644
--- a/schemas/core_1.1.0/select.yml
+++ b/schemas/core_1.1.0/select.yml
@@ -103,80 +103,11 @@ components:
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.
- 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
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
- 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
- tax_id:
- description: ID of the billable entity as recognized by the taxation authority
- type: string
- Address:
- description: Describes a postal address.
- type: string
- 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
- City:
- description: Describes a city
- type: object
- properties:
- name:
- description: Name of the city
- type: string
- code:
- description: City code
- type: string
ItemQuantity:
description: Describes the count or amount of an item
type: object
diff --git a/schemas/jsons/actions.js b/schemas/jsons/actions.js
new file mode 100644
index 0000000..a319983
--- /dev/null
+++ b/schemas/jsons/actions.js
@@ -0,0 +1,13 @@
+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 teh 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/schemas/jsons/context.js b/schemas/jsons/context.js
new file mode 100644
index 0000000..a319983
--- /dev/null
+++ b/schemas/jsons/context.js
@@ -0,0 +1,13 @@
+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 teh 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/schemas/jsons/search.js b/schemas/jsons/search.js
index b9dce5e..68e5ec1 100644
--- a/schemas/jsons/search.js
+++ b/schemas/jsons/search.js
@@ -14,7 +14,7 @@ export default {
properties: {
name: {
type: "string",
- description: "Physical description of the item"
+ description: "shortest search keyword for the item to be searched. For e.g. if someone is looking for tickets for yellowstone national park, search 'tickets'"
}
}
},
@@ -70,7 +70,8 @@ export default {
description: "Describes a GPS coordinate",
pattern: '^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$'
}
- }
+ },
+ required: ['gps']
},
time: {
type: "object",
@@ -80,7 +81,8 @@ export default {
description: "Time of the stop",
format: 'date-time'
}
- }
+ },
+ required: ['timestamp']
},
type:{
type: "string",
diff --git a/services/AI.js b/services/AI.js
index 1b0e55d..a278ada 100644
--- a/services/AI.js
+++ b/services/AI.js
@@ -4,6 +4,7 @@ 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 actions from '../schemas/jsons/actions.js';
const openai = new OpenAI({
apiKey: process.env.OPENAI_AI_KEY,
@@ -16,6 +17,7 @@ class AI {
constructor() {
this.context = [];
this.action = null;
+ this.bookings = [];
}
/**
@@ -24,35 +26,85 @@ class AI {
* @param {*} context
* @returns
*/
- async get_beckn_action_from_text(text, context=[]){
- const openai_messages = [
- { role: 'system', content: `Your job is to analyse the latest user input and check if it is one of the actions given in the following json with their descriptions : ${JSON.stringify(openai_config.SUPPORTED_ACTIONS)}` },
- { role: 'system', content: `You must return a json response with the following structure : {'action':'SOME_ACTION_OR_NULL'}`},
- { role: 'system', content: `Beckn actions must be called in the given order search > select > init > confirm. For e.g. confirm can only be called if init has been called before.`},
- { role: 'system', content: `'action' must be null if its not from the given set of actions. For e.g. planning a trip is not an action. 'find hotels near a place' is a search action.` },
- ...context,
- { role: 'user', content: text }
- ]
+ // async get_beckn_action_from_text(text, context=[], bookings = []){
+ // let booking_context = [
+ // { role: 'system', content: `You must return a json response with the following structure : {'action':'SOME_ACTION_OR_NULL'}`}
+ // ];
+ // if(bookings.length > 0){
+ // booking_context = [
+ // { role: 'system', content: `You must return a json response with the following structure : {'action':'SOME_ACTION_OR_NULL', 'transaction_id':'TRANSACTION_ID_OF_SELECTED_BOOKING_OR_NULL'}.`},
+ // { role: 'system', content: `In case of a 'search', the transaction_id should be taken based on which booking out of the list of bookings the user wants to make. For e.g. if the user wants to book the first booking in the list, the transaction_id should be the transaction_id of the first booking. Booking list : ${JSON.stringify(bookings)}`}
+ // ];
+ // }
+ // const openai_messages = [
+ // { role: 'system', content: `Your job is to analyse the latest user input and check if it is one of the actions given in the following json with their descriptions : ${JSON.stringify(openai_config.SUPPORTED_ACTIONS)}` },
+ // ...booking_context,
+ // { role: 'system', content: `Beckn actions must be called in the given order search > select > init > confirm. For e.g. confirm can only be called if init has been called before.`},
+ // { role: 'system', content: `'action' must be null if its not from the given set of actions. For e.g. planning a trip is not an action. 'find hotels near a place' is a search action.` },
+ // ...context,
+ // { role: 'user', content: text }
+ // ]
+ // let response = {
+ // action: null,
+ // response: null
+ // }
+ // try{
+ // const completion = await openai.chat.completions.create({
+ // messages: openai_messages,
+ // model: process.env.OPENAI_MODEL_ID,
+ // temperature: 0,
+ // response_format: { type: 'json_object' }
+ // })
+ // response = JSON.parse(completion.choices[0].message.content);
+ // }
+ // catch(e){
+ // logger.error(e);
+ // }
+
+ // logger.info(`Got action from text : ${JSON.stringify(response)}`)
+ // return response;
+ // }
+
+ async get_beckn_action_from_text(instruction, context=[], bookings=[]){
let response = {
- action: null,
- response: null
+ action : null
}
+ const messages = [
+ { role: 'system', content: `Supported actions : ${JSON.stringify(openai_config.SUPPORTED_ACTIONS)}` },
+ { role: 'system', content: `Ongoing bookings : ${JSON.stringify(bookings)}` },
+ ...context,
+ { role: "user", content: instruction }
+
+ ];
+
+ const tools = [
+ {
+ type: "function",
+ function: {
+ name: "get_action",
+ description: "Identify if the user wants to perform an action.",
+ parameters: actions
+ }
+ }
+ ];
+
try{
- const completion = await openai.chat.completions.create({
- messages: openai_messages,
+ const gpt_response = await openai.chat.completions.create({
model: process.env.OPENAI_MODEL_ID,
- temperature: 0,
- response_format: { type: 'json_object' }
- })
- response = JSON.parse(completion.choices[0].message.content);
+ messages: 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);
- }
-
- logger.info(`Got action from text : ${JSON.stringify(response)}`)
- return response;
+ return response;
+ }
}
/**
@@ -62,10 +114,10 @@ class AI {
* @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: 'While preparing an itinerary, you should also share a short list of bookings that needs to be made and ask the user which one they want to book first.'},
{ role: 'system', content: `User profile : ${JSON.stringify(profile)}`},
...context,
{ role: 'user', content: instruction}
@@ -78,7 +130,8 @@ class AI {
try{
const completion = await openai.chat.completions.create({
messages: openai_messages,
- model: process.env.OPENAI_MODEL_ID
+ model: process.env.OPENAI_MODEL_ID,
+ max_tokens: 300
})
response = completion.choices[0].message.content;
}
@@ -123,22 +176,34 @@ class AI {
async get_context_by_instruction(instruction, context=[]){
const desired_structure = {
- action: this.action?.action,
- version: 'VERSION_AS_PER_REGISTRY',
- domain:`DOMAIN_AS_PER_REGISTRY_AND_INSTRUCTION_GIVEN_BY_USER`,
+ domain:`DOMAIN_AS_PER_REGISTRY_AND_INSTRUCTION_GIVEN_BY_USER`
+ }
+
+ if(this.action?.action!=='search'){
+ desired_structure.bpp_id = ``;
+ desired_structure.bpp_uri = ``;
+ }
+
+
+ let response = {
message_id : uuidv4(),
transaction_id: uuidv4(),
- base_url: 'AS_PER_REGISTRY',
- bap_id: 'AS_PER_REGISTRY',
- bap_uri: 'AS_PER_REGISTRY',
+ 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, action and registry details and generated a config json in the following structure : ${JSON.stringify(desired_structure)}` },
+ { role: 'system', content: `Your job is to analyse the given instruction, registry details and generated a config json in the following structure : ${JSON.stringify(desired_structure)}` },
{ role: 'system', content: `Registry : ${JSON.stringify(registry_config)}` },
{ role: 'system', content: `Instruction : ${instruction}` },
- { role: 'system', content: `Action : ${this.action?.action}` },
- ...context.filter(c => c.role === 'user')
+ ...context
]
try {
@@ -148,7 +213,8 @@ class AI {
temperature: 0,
response_format: { type: 'json_object' },
})
- let response = JSON.parse(completion.choices[0].message.content)
+ 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) {
@@ -216,14 +282,15 @@ class AI {
}
async get_beckn_message_from_text(instruction, context=[], domain='') {
+ logger.info(`Getting beckn message from instruction : ${instruction}, for domain : ${domain}`)
let domain_context = [], policy_context = [];
- if(domain_context && domain_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)}` }
+ { role: 'system', content: `Use the following policy : ${JSON.stringify(registry_config[0].policies.domains[domain])}` }
]
}
}
@@ -231,7 +298,6 @@ class AI {
const messages = [
...policy_context,
...domain_context,
- { role: "system", content: "Context goes here..."},
...context,
{ role: "user", content: instruction }
@@ -305,14 +371,14 @@ class AI {
}
- async get_text_from_json(json_response, context=[], profile={}) {
+ 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. ',
+ '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.',
@@ -322,14 +388,16 @@ class AI {
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.';
}
+ if(this.bookings.length > 0 && this.action?.action === 'confirm'){
+ call_to_action.confirm=`You should display the order id and show the succesful order confirmation message. You should also show the list of bookings with theri boooking status and ask which booking would they want to do next. Bookings list : ${JSON.stringify(this.bookings)}`
+ }
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)}. If the json is invalid or empty, the status in desired output should be false with the relevant error message.`},
- {role: 'system', content: `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. `},
- {role: 'system', content: `Use this call to action : ${call_to_action[json_response?.context?.action] || ''}`},
+ {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 pforile : ${JSON.stringify(profile)}`},
+ {role: 'system', content: `User profile : ${JSON.stringify(profile)}`},
{role: 'system', content: `Chat history goes next ....`},
- ...context,
+ ...context.slice(-1),
{role: 'assistant',content: `input_json: ${JSON.stringify(json_response)}`},
]
try {
@@ -395,6 +463,91 @@ class AI {
}
}
}
+
+ 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 bookings_updated;
+ }
}
export default AI;
\ No newline at end of file
diff --git a/services/Actions.js b/services/Actions.js
index e7b06c2..134a537 100644
--- a/services/Actions.js
+++ b/services/Actions.js
@@ -89,11 +89,11 @@ class Actions {
// Format the response
logger.info(`Formatting response...`);
- const get_text_from_json_response = await this.ai.get_text_from_json(
+ const format_response_response = await this.ai.format_response(
call_api_response.data,
[...context, { role: 'user', content: message }]
)
- response.formatted = get_text_from_json_response.message
+ response.formatted = format_response_response.message
}
}
} catch (error) {
diff --git a/tests/apis/bot.test.js b/tests/apis/bot.test.js
index c763040..904003a 100644
--- a/tests/apis/bot.test.js
+++ b/tests/apis/bot.test.js
@@ -2,6 +2,7 @@ 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'
const expect = chai.expect
@@ -121,4 +122,90 @@ describe('Test cases for trip planning workflow', ()=>{
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? ",
+ "Just bought a new EV - Chevrolet Bolt, thinking of taking it out for a spin with the family. ",
+ "I'm thinking, Denver to Yellowstone tomorrow for 3 days, traveling with my family of 4 and a pet",
+ "Perfect, lets make the bookings!",
+ "Lets find the hotel first",
+ "Lets select the first one",
+ "Adam, 9999999999, adam@example.com",
+ "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);
+ }
+ })
+
+ it('Should make the ticket bookings', async ()=>{
+
+ const chats = [
+ "lets find tickets for yellowstone national park",
+ "4 of the first one please",
+ "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);
+ }
+ })
+
+ it('Should find and book ev chargers', async ()=>{
+
+ const chats = [
+ "Can you find ev charging stations near me? lat: 48.9762, long: -117.7012",
+ "I've selected the first one",
+ "Sure, go ahead",
+ "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);
+ }
+ })
+
+ it('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);
+ }
+ })
})
\ No newline at end of file
diff --git a/tests/data/chats/trip_planning.json b/tests/data/chats/trip_planning.json
index 23e92ab..7653730 100644
--- a/tests/data/chats/trip_planning.json
+++ b/tests/data/chats/trip_planning.json
@@ -1,5 +1,5 @@
{
- "TRIP_QUERY": "I’m planning a trip from denver to ywllostone national park",
+ "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!",
diff --git a/tests/data/sessions/hotel.json b/tests/data/sessions/hotel.json
index 41e505f..28e0f34 100644
--- a/tests/data/sessions/hotel.json
+++ b/tests/data/sessions/hotel.json
@@ -62,14 +62,6 @@
"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":"Lets find some hotels near Casper",
- "role":"user"
- },
- {
- "content":"Sure! Here are a few pet-friendly hotels near Casper, Wyoming, where you can stay during your trip: 1. La Quinta by Wyndham Casper Address: 400 West F Street, Casper, Wyoming Phone: (307) 232-8541 Website: https://www.wyndhamhotels.com/laquinta/casper-wyoming/la-quinta-casper/overview 2. Holiday Inn Express Casper Address: 4250 Legion Ln, Casper, Wyoming Phone: (307) 577-5000 Website: https://www.ihg.com/holidayinnexpress/hotels/us/en/casper/cprwy/hoteldetail 3. 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 These hotels offer pet-friendly accommodations and are conveniently located near Casper. You can check their availability and book a room for your stay. Let me know if you need more options or information.",
- "role":"assistant"
- },
{
"content":"Can you find some hotels near Casper?",
"role":"user"
diff --git a/tests/unit/services/ai.test.js b/tests/unit/services/ai.test.js
index 110befa..4c7d987 100644
--- a/tests/unit/services/ai.test.js
+++ b/tests/unit/services/ai.test.js
@@ -26,7 +26,7 @@ describe('Test cases for services/ai/get_beckn_action_from_text()', () => {
expect(response.action).to.be.null
})
- it('Should return null action when asked about list of bookings to be done', async () => {
+ it.skip('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
@@ -86,17 +86,27 @@ describe('Test cases for services/ai/get_beckn_action_from_text()', () => {
expect(response.action).to.equal('search');
});
- it('Should return `clear_chat` action when user wishes to clear the chat', async () => {
- const response = await ai.get_beckn_action_from_text('Can you clear this session ', hotel_session.data.actions.formatted);
+ it.skip('Should return `clear_chat` action when user wishes to clear the chat', async () => {
+ const response = await ai.get_beckn_action_from_text('Can you clear my chat?', hotel_session.data.actions.formatted);
expect(response).to.have.property('action')
expect(response.action).to.equal('clear_chat');
});
- it('Should return `clear_all` action when user wishes to clear the the entire session including profile.', async () => {
- const response = await ai.get_beckn_action_from_text('Can you clear this session along with my profile.', hotel_session.data.actions.formatted);
+ it.skip('Should return `clear_all` action when user wishes to clear the the entire session including profile.', async () => {
+ const response = await ai.get_beckn_action_from_text('Can you clear my entire session?', hotel_session.data.actions.formatted);
expect(response).to.have.property('action')
expect(response.action).to.equal('clear_all');
});
+
+ it.skip('Should return `booking_collection` action If the user wants to make multiple bookings', async () => {
+ const context = [
+ {role: 'user', content: trip_planning.TRIP_DETAILS},
+ {role: 'assistant', content: trip_planning.TRIP_DETAILS_RESPONSE}
+ ];
+ const response = await ai.get_beckn_action_from_text('Perfect! can you make the bookings?', context);
+ expect(response).to.have.property('action')
+ expect(response.action).to.equal('booking_collection');
+ });
})
describe('Test cases for get_ai_response_to_query() function', () => {
@@ -295,16 +305,16 @@ describe('Test cases for services/ai/get_beckn_request_from_text()', () => {
});
-describe('Test cases for services/ai/get_text_from_json()', () => {
- it('Should test get_text_from_json() and throw response with success false for empty object', async () => {
- const response = await ai.get_text_from_json({})
+describe('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 get_text_from_json() return some message with success true', async () => {
+ 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.get_text_from_json(on_search, context)
+ const response = await ai.format_response(on_search, context)
expect(response.status).to.equal(true)
})
})