Skip to content

Commit

Permalink
Merge branch 'main' of github.com:beckn/beckn-action-bot into feat/cr…
Browse files Browse the repository at this point in the history
…eate_control_center
  • Loading branch information
mayurvir committed Mar 31, 2024
2 parents 547c043 + c23339c commit 0ab2ff4
Show file tree
Hide file tree
Showing 21 changed files with 1,225 additions and 1,779 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/api_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run api tests

on:
push:
branches-ignore:
- main
jobs:
test-lint:
name: API tests
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up environment variables
run: |
echo "OPENAI_AI_KEY=${{secrets.OPENAI_AI_KEY}}" >> .env
echo "OPENAI_MODEL_ID=${{secrets.OPENAI_MODEL_ID}}" >> .env
echo "OPEN_AI_EMBEDDINGS_MODEL=${{secrets.OPEN_AI_EMBEDDINGS_MODEL}}" >> .env
echo "PORT=${{secrets.PORT}}" >> .env
echo "SERVER_PORT=${{secrets.SERVER_PORT}}" >> .env
echo "TWILIO_ACCOUNT_SID=${{secrets.TWILIO_ACCOUNT_SID}}" >> .env
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Run api tests
run: npm run docker:test:apis
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Run linting and tests
name: Run linting tests

on:
push:
branches-ignore:
- main
jobs:
test-lint:
name: Test
name: Lint checks
runs-on: ubuntu-20.04
steps:
- name: Checkout code
Expand Down Expand Up @@ -34,6 +34,3 @@ jobs:

- name: Run linting
run: npm run docker:lint

- name: Run tests
run: npm run docker:test
36 changes: 36 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run unit tests

on:
push:
branches-ignore:
- main
jobs:
test-lint:
name: Unit tests
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up environment variables
run: |
echo "OPENAI_AI_KEY=${{secrets.OPENAI_AI_KEY}}" >> .env
echo "OPENAI_MODEL_ID=${{secrets.OPENAI_MODEL_ID}}" >> .env
echo "OPEN_AI_EMBEDDINGS_MODEL=${{secrets.OPEN_AI_EMBEDDINGS_MODEL}}" >> .env
echo "PORT=${{secrets.PORT}}" >> .env
echo "SERVER_PORT=${{secrets.SERVER_PORT}}" >> .env
echo "TWILIO_ACCOUNT_SID=${{secrets.TWILIO_ACCOUNT_SID}}" >> .env
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Run unit tests
run: npm run docker:test:unit
32 changes: 8 additions & 24 deletions config/openai.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
{
"SUPPORTED_ACTIONS": [
{ "key": "search", "description": "Perform a search" },
{ "key": "select", "description": "The instruction declares the customer's cart (or equivalent) created by selecting objects from the catalog. This is equivalent of adding items to cart (or equivalent) by selecting them." },
{ "key": "init", "description": "Initialize an order by providing billing and/or shipping details." },
{ "key": "confirm", "description": "Confirm an action" },
{ "key": "status", "description": "Get the status of an action" },
{ "key": "track", "description": "Track an action" },
{ "key": "cancel", "description": "Cancel an action" },
{ "key": "update", "description": "Update an action" },
{ "key": "rating", "description": "Provide a rating" },
{ "key": "support", "description": "Get support" }
],
"SUPPORTED_DOMAINS": [
{"key": "uei:charging", "description": "Used for energy transactions"}
{ "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", "description": "If the user wants to clear the session or restart session or chat." }
],
"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':''}'"},
{ "role": "system", "content": "A typical order flow should be search > select > init > confirm."},
{ "role": "system", "content": "Auto-generate uuid wherever required and should be a valid uuid such as ."},
{ "role": "system", "content": "Use the context to identify the transaction id returned in search api response and use it in subsequent api calls."},
{ "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 from assistant for filling transaction_id, bpp_id, bpp_uri in the context of all calls except `search`."},
{ "role": "system", "content": "Use the response from assistant to select items from the list of items provided by the assistant."}

],
"PRESETS" : {
"bap_id": "mit-ps-bap.becknprotocol.io",
"bap_uri": "https://mit-ps-bap.becknprotocol.io",
"version": "1.1.0",
"base_url": "https://mit-ps-bap-client.becknprotocol.io"
}
{ "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."}
]
}
16 changes: 16 additions & 0 deletions config/registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"url": "https://mit-ps-bap-client.becknprotocol.io",
"domains": [
"uei:charging",
"retail:1.1.0",
"hospitality",
"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 and pet supplies, hospitality for hotels, 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"
}
]
197 changes: 176 additions & 21 deletions controllers/Bot.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,63 @@
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 twilio from 'twilio'

const { MessagingResponse } = twilio.twiml
const actionsService = new ActionsService()
const db = new DBService();

/**
* @deprecated
* @param {*} req
* @param {*} res
*/
async function process_wa_webhook(req, res) {
try {
const message = req.
body.Body
const message = req.body.Body
const sender = req.body.From
const format = req.headers['content-type'] || 'text/xml';
const twiml = new MessagingResponse();

const raw_yn = req.body.raw_yn || false;

const EMPTY_SESSION = {
sessionId: sender,
data : []
}

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 = {
sessionId: sender,
data : []
}
session = EMPTY_SESSION
}


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

// Process instruction
const process_response = await actionsService.process_instruction(message, session.data)

if(process_response.formatted && process_response.raw && typeof process_response.raw === 'object'){
if (process_response.raw?.context?.action === 'search') {
session = EMPTY_SESSION
}
if(process_response.formatted){
session.data.push({ role: 'user', content: message }); // add user message to session
session.data.push({ role: 'assistant', content: JSON.stringify(process_response.raw) }); // add system response to session
if(process_response.raw && typeof process_response.raw === 'object'){
session.data.push({ role: 'assistant', content: JSON.stringify(process_response.raw) }); // add system response to session
}
else{
session.data.push({ role: 'assistant', content: process_response.formatted }); // add system response to session
}

await db.update_session(sender, session);
}

twiml.message(process_response.formatted)

// twiml.message(process_response.formatted)
logger.info(`Sending formatted response to ${sender}: ${process_response.formatted}`)
if(format!='application/json'){
res.type('text/xml').send(twiml.toString())
// res.type('text/xml').send(twiml.toString())
actionsService.send_message(sender, process_response.formatted)
res.send("Done!")
}
else{
res.send(process_response.formatted)
raw_yn ? res.send(process_response.raw) : res.send(process_response.formatted)
}

} catch (error) {
Expand All @@ -52,6 +66,147 @@ async function process_wa_webhook(req, 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();

// inputs
const 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;

let response= {
raw: null,
formatted: null
};

const EMPTY_SESSION = {
sessionId: sender,
text : [],
actions : {
raw: [],
formatted: []
}
}

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{
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'){
session = EMPTY_SESSION;
response.formatted = 'Session 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}`);

// update session
session.text.push({ role: 'user', content: message });
session.text.push({ role: 'assistant', content: response.formatted });
}
else{
response = await process_action(ai.action, message, session.actions);

// update actions
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 });
}
}

// update session
await db.update_session(sender, session);

// Send response
if(format!='application/json'){
actionsService.send_message(sender, response.formatted)
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 {*} actions_context
* @returns
*/
async function process_action(action, text, actions_context){
let ai = new AI();
let response = {
raw: null,
formatted: null
}

ai.action = action;

// Get schema
const schema = await ai.get_schema_by_action(action.action);

// Get config
const beckn_context = await ai.get_context_by_instruction(text, actions_context.raw);

// Prepare request
if(schema && beckn_context){
const request = await ai.get_beckn_request_from_text(text, actions_context.raw, beckn_context, schema);

if(request.status){
// call api
const api_response = await actionsService.call_api(request.data.url, request.data.method, request.data.body, request.data.headers)
if(!api_response.status){
response.formatted = `Failed to call the API: ${api_response.error}`
}
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(
api_response.data,
[...actions_context.formatted, { role: 'user', content: text }]
);
response.formatted = formatted_response.message;
}
}
else{
response.formatted = "Could not prepare this request. Can you please try something else?"
}
}

return response;
}

export default {
process_wa_webhook,
process_text
}
Loading

0 comments on commit 0ab2ff4

Please sign in to comment.