diff --git a/controllers/Bot.js b/controllers/Bot.js index 8ec1e61..41f31aa 100644 --- a/controllers/Bot.js +++ b/controllers/Bot.js @@ -1,22 +1,40 @@ import ActionsService from '../services/Actions.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(); async function process_wa_webhook(req, res) { try { - const messageBody = req.body.Body + const message = req.body.Body const sender = req.body.From - const twiml = new MessagingResponse() + const twiml = new MessagingResponse(); - logger.info(`Received message from ${sender}: ${messageBody}`) + // 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 : [] + } + } - const responseMessage = - await actionsService.process_instruction(messageBody) - twiml.message(responseMessage) + logger.info(`Received message from ${sender}: ${message}`) + + const process_response = await actionsService.process_instruction(message, session.data) + + if(process_response.status && process_response.message){ + session.data.push({ role: 'user', content: message }); // add user message to session + session.data.push({ role: 'assistant', content: process_response.message }); // add system response to session + await db.update_session(sender, session); + } + + twiml.message(process_response.message) res.type('text/xml').send(twiml.toString()) } catch (error) { diff --git a/package.json b/package.json index 268145b..91c36c4 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "NODE_ENV=test mocha tests --recursive --timeout 900000 -r dotenv/config --exit", "test:unit": "NODE_ENV=test mocha tests/unit --recursive --timeout 0 -r dotenv/config --exit", "test:apis": "NODE_ENV=test mocha tests/apis --recursive --timeout 0 -r dotenv/config --exit", - "start": "nodemon server.js", + "start": "nodemon --env-file=.env server.js", "dev": "NODE_ENV=dev && npm start", "prod": "NODE_ENV=prod && npm start", "prettify": "prettier --write .", diff --git a/server.js b/server.js index 674a85a..d398bcd 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ 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' const app = express() @@ -17,6 +18,10 @@ app.use(bodyParser.json()) // app.post('/act', actions.act) app.post('/webhook', messageController.process_wa_webhook) +// Reset all sessions +const db = new DBService(); +await db.clear_all_sessions(); + // Start the Express server app.listen(process.env.SERVER_PORT, () => { logger.info(`Server is running on port ${process.env.SERVER_PORT}`) diff --git a/services/AI.js b/services/AI.js index f810703..7f15225 100644 --- a/services/AI.js +++ b/services/AI.js @@ -177,7 +177,7 @@ class AI { return response; } - async get_message_from_beckn_response(json_response) { + async get_text_from_json(json_response) { const openai_messages = [ { role: 'system', @@ -187,7 +187,7 @@ class AI { try { const completion = await openai.chat.completions.create({ messages: openai_messages, - model: process.env.OPENAI_MODEL_ID, + model: 'gpt-4-0125-preview', // using a higher token size model temperature: 0, response_format: { type: 'json_object' }, }) diff --git a/services/Actions.js b/services/Actions.js index 0c2ce0a..a1c5fab 100644 --- a/services/Actions.js +++ b/services/Actions.js @@ -1,6 +1,7 @@ import twilio from 'twilio' import logger from '../utils/logger.js' import axios from 'axios' +import AI from './AI.js' const accountSid = process.env.TWILIO_ACCOUNT_SID const authToken = process.env.TWILIO_AUTH_TOKEN @@ -11,6 +12,7 @@ const client = twilio(accountSid, authToken) class Actions { constructor() { + this.ai = new AI() this.context = []; } @@ -45,15 +47,43 @@ class Actions { return responseObject } - async process_instruction(messageBody) { + async process_instruction(message, context=[]) { + let response = { + status: false, + message: 'Failed to process the instruction', + } try { - return `You said "${messageBody}"` + + // Get becnk request from text message + const beckn_request = await this.ai.get_beckn_request_from_text(message, context); + if(!beckn_request.status){ + response.message = beckn_request.message; + } + else{ + // Call the API + const call_api_response = await this.call_api(beckn_request.data.url, beckn_request.data.method, beckn_request.data.body, beckn_request.data.headers) + if(!call_api_response.status){ + response.message = `Failed to call the API: ${call_api_response.error}` + response.data = call_api_response.data + } + else{ + + // Format the response + const get_text_from_json_response = await this.ai.get_text_from_json(call_api_response.data) + response = { + status: true, + message: get_text_from_json_response.message + } + } + } } catch (error) { - logger.error(`Error processing instruction: ${error.message}`) - throw new Error(`Failed to process the instruction: ${error.message}`) + logger.error(`Error processing instruction: ${error.message}`) + response.message = `Failed to process the instruction: ${error.message}` } + + return response; } - + async send_message(recipient, message) { try { await client.messages.create({ diff --git a/services/DBService.js b/services/DBService.js new file mode 100644 index 0000000..181ae15 --- /dev/null +++ b/services/DBService.js @@ -0,0 +1,122 @@ +import logger from '../utils/logger.js' +import redis from 'redis' + +class DBService { + + constructor() { + let redisUrl = process.env.REDIS_URL || 'redis://localhost:6379' + this.redisClient = redis.createClient({ url: redisUrl }) + + this.redisClient.on('error', (err) => { + logger.error('Redis Client Error', err) + }) + + this.redisClient.connect() + } + + /** + * Get session using Redis + * @param {*} sessionId + * @returns + */ + async get_session(sessionId) { + let response = { + status: false, + } + try { + let sessionData = await this.redisClient.get(sessionId) + if (sessionData === null) { + response.status = false + response.message = 'Session does not exist!' + } else { + response.status = true + response.message = 'Session retrieved successfully!' + response.data = JSON.parse(sessionData) + } + } catch (err) { + logger.error(err) + response.error = err + } + + logger.info(response) + return response + } + + /** + * Deletes a session using Redis + * @param {*} sessionId + * @returns + */ + async delete_session(sessionId) { + let response = { + status: false, + } + try { + let deleteResponse = await this.redisClient.del(sessionId) + if (deleteResponse === 0) { + response.status = false + response.message = 'Session does not exist!' + } else { + response.status = true + response.message = 'Session deleted successfully!' + } + } catch (err) { + logger.error(err) + response.error = err + } + + logger.info(response) + return response + } + + /** + * Function to clear all sessions + * @returns + */ + async clear_all_sessions(){ + let response = { + status: false, + } + try { + let deleteResponse = await this.redisClient.flushAll() + if (deleteResponse === 0) { + response.status = false + response.message = 'Sessions do not exist!' + } else { + response.status = true + response.message = 'Session flushed successfully!' + } + } catch (err) { + logger.error(err) + response.error = err + } + + logger.info(response) + return response + } + + /** + * Updates a session + * @param {*} sessionId + * @param {*} sessionData + * @returns + */ + async update_session(sessionId, sessionData) { + let response = { + status: false, + } + try { + await this.redisClient.set(sessionId, JSON.stringify(sessionData)) + response.status = true + response.message = 'Session updated successfully!' + } catch (err) { + logger.error(err) + response.error = err + } + + logger.info(response) + return response + } +} + +export default DBService; \ No newline at end of file diff --git a/tests/unit/services/actions.test.js b/tests/unit/services/actions.test.js index b749cc0..4b592e5 100644 --- a/tests/unit/services/actions.test.js +++ b/tests/unit/services/actions.test.js @@ -9,7 +9,7 @@ describe('Test cases for services/actions.js', () => { it('should process the instruction message', async () => { const messageBody = 'test message'; const result = await actionsService.process_instruction(messageBody); - expect(result).to.equal('You said "test message"'); + expect(result.message).to.equal('You said "test message"'); }); }) diff --git a/tests/unit/services/ai.test.js b/tests/unit/services/ai.test.js index 772bb76..d863a79 100644 --- a/tests/unit/services/ai.test.js +++ b/tests/unit/services/ai.test.js @@ -140,14 +140,14 @@ describe('Test cases for services/ai/get_beckn_request_from_text()', () => { }); -describe('Test cases for services/ai/get_message_from_beckn_response()', () => { - it('Should test get_message_from_beckn_response() and throw response with success false for empty object', async () => { - const response = await ai.get_message_from_beckn_response({}) +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({}) expect(response.success).to.equal(false) expect(response.message).to.equal('Empty JSON') }) - it('Should test get_message_from_beckn_response() return some message with success true', async () => { - const response = await ai.get_message_from_beckn_response(on_init) + it('Should test get_text_from_json() return some message with success true', async () => { + const response = await ai.get_text_from_json(on_init) expect(response.success).to.equal(true) }) }) \ No newline at end of file