Skip to content

Commit

Permalink
Merge pull request #64 from beckn/add_search_filters
Browse files Browse the repository at this point in the history
[Feature] Added search filters
  • Loading branch information
mayurvir authored Apr 6, 2024
2 parents 18e46a8 + c835f65 commit abd42c3
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ seeders/*
models/*
package*.json
LICENSE
*.md
*.md
schemas/*
27 changes: 26 additions & 1 deletion config/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@
"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",
"bap_subscriber_id": "mit-ps-bap.becknprotocol.io",
"bap_subscriber_url": "https://mit-ps-bap.becknprotocol.io",
"version": "1.1.0"
"version": "1.1.0",
"policies": {
"domains": {
"uei:charging": {
"tags": [
{ "vehicle-type": { "enum": ["2-wheeler", "4-wheeler"]}},
{ "connector-type": { "enum": ["CCS", "CHAdeMo"]}}
],
"rules": [
"intent is not needed for this domain",
"search should have fulfillment for this domain."
]
},
"hospitality": {
"tags": [
{ "pet-friendly": { "enum": ["yes", "no"]}},
{ "ev-charging": { "enum": ["yes", "no"]}},
{ "accomodation-type“": { "enum": ["campsite", "hotel", "independent-house"]}}
],
"rules": [
"search must have two stops for this domain.",
"Supported stop.type : check-in, check-out"
]
}
}
}
}
]
26 changes: 24 additions & 2 deletions controllers/Bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async function process_text(req, res) {
let ai = new AI();

// inputs
const message = req.body.Body
let message = req.body.Body
const sender = req.body.From
const format = req.headers['content-type'] || 'text/xml';
const raw_yn = req.body.raw_yn || false;
Expand All @@ -94,6 +94,11 @@ async function process_text(req, res) {
formatted: []
}
}

// Update lat, long
if(req.body.Latitude && req.body.Longitude){
message+=` lat:${req.body.Latitude} long:${req.body.Longitude}`
}

logger.info(`Received message from ${sender}: ${message}. Response format: ${format}`)

Expand Down Expand Up @@ -210,7 +215,24 @@ async function process_action(action, text, session, sender=null){

// Prepare request
if(schema && beckn_context){
const request = await ai.get_beckn_request_from_text(text, session.actions.raw, beckn_context, schema);
let request=null;
if(ai.action.action==='search'){
const message = await ai.get_beckn_message_from_text(text, session.text, beckn_context.domain);
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, session.actions.raw, beckn_context, schema);
}

if(request.status){
// call api
Expand Down
97 changes: 97 additions & 0 deletions schemas/jsons/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export default {
type: "object",
properties: {
intent: {
type: "object",
description: "The intent to buy a product or avail a service.",
properties:{
item: {
type: "object",
description: "The product or service that the user wants to buy or avail.",
properties: {
descriptor: {
type: "object",
properties: {
name: {
type: "string",
description: "Physical description of the item"
}
}
},
tags: {
type: "array",
description: "List of tags that the user wants to search by. This should be defined by the network policy",
items: {
type: "object",
properties: {
list: {
type: "array",
description: "List of tags",
items: {
type: "object",
properties: {
descriptor: {
type: "object",
properties: {
code: {
type: "string",
description: "code of the tag"
}
}
},
value: {
type: "string",
description: "value of the tag"
}
}
}
}
}
}

}
}
},
fulfillment:{
type: "object",
description: "The fulfillment details of the item",
properties: {
stops: {
type: "array",
description: "List of stops",
items: {
type: "object",
properties: {
location: {
type: "object",
properties: {
gps: {
type: "string",
description: "Describes a GPS coordinate",
pattern: '^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$'
}
}
},
time: {
type: "object",
properties: {
timestamp: {
type: "string",
description: "Time of the stop",
format: 'date-time'
}
}
},
type:{
type: "string",
description: "The type of stop. Allowed values of this property can be defined by the network policy.",
}
}
}
}
}
}
}
}
}
}
52 changes: 52 additions & 0 deletions services/AI.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';

const openai = new OpenAI({
apiKey: process.env.OPENAI_AI_KEY,
Expand Down Expand Up @@ -213,6 +214,57 @@ class AI {

return action_response;
}

async get_beckn_message_from_text(instruction, context=[], domain='') {
let domain_context = [], policy_context = [];
if(domain_context && domain_context!='') {
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)}` }
]
}
}

const messages = [
...policy_context,
...domain_context,
{ role: "system", content: "Context goes here..."},
...context,
{ role: "user", content: instruction }

];

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)}`);
return responseMessage
}
catch(e){
logger.error(e);
return null;
}
}

async compress_search_results(search_res){

Expand Down
41 changes: 40 additions & 1 deletion tests/unit/services/ai.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('Test cases for services/ai/get_beckn_action_from_text()', () => {
expect(response.action).to.be.null
});

it('Should return search action when user searches after a long context', async () => {
it.skip('Should return search action when user searches after a long context', async () => {
const response = await ai.get_beckn_action_from_text('Can you find some hotels near Casper ', hotel_session.data.actions.formatted);
expect(response).to.have.property('action')
expect(response.action).to.equal('search');
Expand Down Expand Up @@ -309,6 +309,45 @@ describe('Test cases for services/ai/get_text_from_json()', () => {
})
})

describe('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)
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');
})

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')
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');
})

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')
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');
})

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')
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('Test cases for get_profile_from_text', () => {
it('Should return an object with billing details if billing details shared', async ()=> {
Expand Down

0 comments on commit abd42c3

Please sign in to comment.