From 39d03d4b3a69a3d2904b256b772ab29c5b549411 Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Thu, 21 Aug 2025 22:22:47 -0500 Subject: [PATCH] updating proactive agent to support both copilot and messenger front-ends --- .../GlobalVariable/copilot_overrides.json | 26 ++ .../proactive_agent_settings.json | 21 + .../ProxyScript/copilot.js | 86 ++-- .../messenger_667ef98d3ee8930a5debcbdb.js | 175 ++++++++ .../ProxyScript/proactive_agent.js | 382 ++++++++++++++++++ copilot/copilot-extension/manifest.json | 2 +- 6 files changed, 655 insertions(+), 37 deletions(-) create mode 100644 copilot/_studio_dependencies/GlobalVariable/copilot_overrides.json create mode 100644 copilot/_studio_dependencies/GlobalVariable/proactive_agent_settings.json create mode 100644 copilot/_studio_dependencies/ProxyScript/messenger_667ef98d3ee8930a5debcbdb.js create mode 100644 copilot/_studio_dependencies/ProxyScript/proactive_agent.js diff --git a/copilot/_studio_dependencies/GlobalVariable/copilot_overrides.json b/copilot/_studio_dependencies/GlobalVariable/copilot_overrides.json new file mode 100644 index 0000000..bc89ad5 --- /dev/null +++ b/copilot/_studio_dependencies/GlobalVariable/copilot_overrides.json @@ -0,0 +1,26 @@ +[ + { + "v_username": "Peter.Morton@verint.com", + "copilot_settings": { + "ops": { + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJ3b3Jrc3BhY2VJZCI6IjY4OWE2MjlkZmI3NjYyMzI3YWMyNWFhMSIsInNlcnZpY2UiOiJPcHMiLCJpc0FwaVRva2VuIjp0cnVlLCJpYXQiOjE3NTQ5NDgyNTN9.tGEWgFbvioKs7y9pi9pCkl_OGRy2YmyMvOaCpWFlo_9HvWUAaFeGL5phfGw8gDlPzcBlFbr1RxmEbCZaiRs1rHlGZJnpXHU9klKj_1tj6Cg7PfA-BA7YhPnZAOTpbKxeV8NYAc3SoWYZuy4l3NoXOaD3_f5FtpgmcEmUu7pvKWl4t4Q9DhXtHYnzAEoR9N3pA9jVVpcyZV9RG491mP_XOpjtP1sh3TgyGeakyBAwzOfJZn17Yzs2faqwfbP7Hy3shzCCEWkLy-3MMukDPOkL6D7Yx_NCM-P8Rdi3rCWJLpAk9Xsql8uPdOOlW78evUTXbDtZl4tndJdwK9VFmVjG2w" + }, + "nlu": { + "apiBaseURL": "https://router.ivastudio.verint.live/" + }, + "channel": "copilot", + "messengerRouteName": "messenger_667ef98d3ee8930a5debcbdb" + }, + "req": { + "params": { + "workspaceId": "67bca862210071627d32ef12", + "branch": "current" + } + }, + "event": { + "params": { + "modelName": "main" + } + } + } +] \ No newline at end of file diff --git a/copilot/_studio_dependencies/GlobalVariable/proactive_agent_settings.json b/copilot/_studio_dependencies/GlobalVariable/proactive_agent_settings.json new file mode 100644 index 0000000..d2be0ff --- /dev/null +++ b/copilot/_studio_dependencies/GlobalVariable/proactive_agent_settings.json @@ -0,0 +1,21 @@ +{ + "messenger": { + "user": { + "name": "Proactive Agent", + "avatar": "https://storage.googleapis.com/speakeasyai-agent-desktop/img/verint-logo.png", + "userId": "d4de56b5-3fac-4a42-892f-ebec914b6aa3", + "type": "bot" + } + }, + "modelName": "proactiveAgent", + "copilot": { + "customerId": "78ab8a80-88a3-4db0-a3f9-36e7aeef18db", + "tokenUrl": "https://api.verint.com/vcp/identity-dev/integration/auth/v2/oauth/token", + "oauth2": { + "grant_type": "client_credentials", + "clientId": "XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "clientSecret": "XXXXXXXXXXXX", + "audience": "api://vcp/wa-cloudevent" + } + } +} \ No newline at end of file diff --git a/copilot/_studio_dependencies/ProxyScript/copilot.js b/copilot/_studio_dependencies/ProxyScript/copilot.js index eb998bc..8dae571 100644 --- a/copilot/_studio_dependencies/ProxyScript/copilot.js +++ b/copilot/_studio_dependencies/ProxyScript/copilot.js @@ -1,10 +1,52 @@ /* Proxy Script for CoPilot integration with IVAS */ -console.log({ script: "copilot", body: req.body}); +console.log({ script: "copilot", body: req.body }); + +let copilot_settings = copilot_settings; + +if (req.headers.authorization) { + try { + console.log("decoding authorization ..."); + let bearer = req.headers.authorization.replace("Bearer ", ""); + + req.body.event.metadata = { + ...req.body.event.metadata, + ...{ + bearer: jwt.decode(bearer), + }, + }; + + console.log("applying copilot overrides ..."); + const found = copilot_overrides.find( + (element) => + element.v_username === req.body.event.metadata.bearer.v_username + ); + if (found) { + console.log("found copilot overrides for user: " + found.v_username); + copilot_settings = { + ...copilot_settings, + ...found.copilot_settings, + }; + req.params = { ...req.params, ...found.req.params }; + req.body.event.params = { + ...req.body.event.params, + ...found.event.params, + }; + } else { + console.log( + "no copilot overrides found for user: " + + req.body.event.metadata.bearer.v_username + ); + } + } catch (e) { + console.log("Error decoding authorization:"); + console.log(e); + } +} req.body.event.metadata = _.merge(req.body.event.metadata, { - channel: copilot_settings.channel + channel: copilot_settings.channel, }); const messengerUrl = `${copilot_settings.nlu.apiBaseURL}ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/${copilot_settings.messengerRouteName}`; @@ -14,44 +56,16 @@ const messengerUrl = `${copilot_settings.nlu.apiBaseURL}ProxyScript/run/${req.pa url: messengerUrl, method: "post", data: req.body, - // headers: { - // Authorization: `Bearer ${copilot_settings.ops.token}` - // } + // headers: { + // Authorization: `Bearer ${copilot_settings.ops.token}` + // } }); res.send(recognizedData); } catch (error) { console.log(error.message); res.send({ - answers: ['Something went wrong. Please try again.'], - outputs: {} + answers: ["Something went wrong. Please try again."], + outputs: {}, }); } -})();/* - Proxy Script for CoPilot integration with IVAS -*/ -console.log({ script: "copilot", body: req.body}); - -req.body.event.metadata = _.merge(req.body.event.metadata, { - channel: copilot_settings.channel -}); - -const messengerUrl = `${copilot_settings.nlu.apiBaseURL}ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/${copilot_settings.messengerRouteName}`; -(async () => { - try { - const { data: recognizedData } = await axios({ - url: messengerUrl, - method: "post", - data: req.body, - // headers: { - // Authorization: `Bearer ${copilot_settings.ops.token}` - // } - }); - res.send(recognizedData); - } catch (error) { - console.log(error.message); - res.send({ - answers: ['Something went wrong. Please try again.'], - outputs: {} - }); - } -})(); \ No newline at end of file +})(); diff --git a/copilot/_studio_dependencies/ProxyScript/messenger_667ef98d3ee8930a5debcbdb.js b/copilot/_studio_dependencies/ProxyScript/messenger_667ef98d3ee8930a5debcbdb.js new file mode 100644 index 0000000..a1a1bfc --- /dev/null +++ b/copilot/_studio_dependencies/ProxyScript/messenger_667ef98d3ee8930a5debcbdb.js @@ -0,0 +1,175 @@ +console.log('>>> messenger >>>') ; +console.log( { query: req.query, params: req.params, body: req.body }) ; +console.log('>>> messenger >>>') ; +const external = req.body.event?.external; +const sessionId = req.body.event?.conversationId; +const input = req.body.event?.input; +const params = req.body.event?.params || {}; +const modelName = params?.modelName || settings_667ef98d3ee8930a5debcbdb.nlu.modelName; +const env = params?.env; +const postBack = req.body.event?.postBack; +const metadata = { + ...req.body.event?.metadata, + userId: req.body.event?.sentBy?.userId +}; +const configuration = req.body.event?.configuration; +(async () => { + try { + const smStr = `session-map-${sessionId}`; + if (external) {} else { + await axios.post(`${settings_667ef98d3ee8930a5debcbdb.messenger.apiBaseURL}Conversation/join`, { + _id: sessionId, + participant: { + userId: settings_667ef98d3ee8930a5debcbdb.messenger.participantId, + type: 'bot', + name: settings_667ef98d3ee8930a5debcbdb.messenger.name, + avatar: settings_667ef98d3ee8930a5debcbdb.messenger.avatar + } + }, { + headers: { + Authorization: `Bearer ${settings_667ef98d3ee8930a5debcbdb.messenger.token}` + } + }).then(response => { + if (response?.data?._id) {} else throw new Error(); + }); + axios.post(`${settings_667ef98d3ee8930a5debcbdb.messenger.apiBaseURL}Event/isTyping`, { + workspaceId: req.params.workspaceId, + conversationId: sessionId, + userId: settings_667ef98d3ee8930a5debcbdb.messenger.participantId, + typing: true + }, { + headers: { + Authorization: `Bearer ${settings_667ef98d3ee8930a5debcbdb.messenger.token}` + } + }); + } + const sessionMap = await redis.hGetAll(smStr); + console.log(smStr); + console.log(sessionMap); + const recognizedData = await axios.post(`${settings_667ef98d3ee8930a5debcbdb.nlu.apiBaseURL}Model/run/${req.params.workspaceId}/${req.params.branch}/${modelName}`, { + input: input, + conversationId: sessionMap.conversationId, + settings: settings_667ef98d3ee8930a5debcbdb.nlu.settings, + metadata: metadata, + postBack: postBack, + configuration: configuration + }).then(response => { + return response.data; + }).catch(error => { + //If we have error handling turned off, don't record the error + if (settings_667ef98d3ee8930a5debcbdb.errorHandling === false) { + throw error; + } else { + // Conversation ID is generated by the NLU + // Must make sure Conversation ID is created properly for an error transaction + // Get it from session map if it exists, or generate it if we got the error at the very first transaction. + const conversationId = sessionMap.conversationId || uuidv4(); + let errorPayload = { + id: uuidv4(), + workspaceId: req.params.workspaceId, + errorInfo: { + name: error.name, + message: error.message, + stack: error.stack + }, + conversationId, + input, + answers: [settings_667ef98d3ee8930a5debcbdb.responses.modelError], + req: { + params: req.params, + query: req.query, + body: req.body, + headers: req.headers + }, + metadata, + classificationResults: [{ + label: "Model Error", + value: 0.0 + }], + nerResults: { + entities: [], + sourceEntities: [] + }, + // Reporting tags to match GlobalSupport function reporting + tag: "ERROR", + reporting: { + tag: "ERROR", + modelResponse: "ERROR" + } + }; + + // Add model name + errorPayload.req.params.route = modelName; + + //add any other details you wish to see in transaction data + + return errorPayload; + } + }); + // console.log(recognizedData); + if (recognizedData.conversationId) { + await redis.hSet(smStr, 'conversationId', recognizedData.conversationId); + await redis.expire(smStr, 3600); + if (sessionMap.conversationId) {} else { + db.analytics.addConversation({ + id: recognizedData.conversationId + }); + } + } + // NOTE: Additonal logic could be used to provide answer from alternative sources + // recognizedData.answers = API_RESPONSE || ['Example']; + if (recognizedData.tag === "ERROR") {} else if (recognizedData.classificationResults.length === 0 && recognizedData.answers.length === 0) { + recognizedData.answers = [settings_667ef98d3ee8930a5debcbdb.responses.unrecognized]; + } else if (recognizedData.answers.length === 0) { + recognizedData.answers = [settings_667ef98d3ee8930a5debcbdb.responses.unanswered]; + } + recognizedData._id = (await db.analytics.addTransaction(recognizedData)).insertedId; + if (external) {} else { + new Promise(async resolve => { + for (const [index, answer] of recognizedData.answers.entries()) { + try { + await axios.post(`${settings_667ef98d3ee8930a5debcbdb.messenger.apiBaseURL}Event/create`, { + conversationId: sessionId, + input: answer, + sentBy: { + userId: settings_667ef98d3ee8930a5debcbdb.messenger.participantId, + name: settings_667ef98d3ee8930a5debcbdb.messenger.name, + avatar: settings_667ef98d3ee8930a5debcbdb.messenger.avatar + }, + ping: nanoid(), + sentAt: Date.now(), + options: recognizedData?.options, + metadata: { + outputs: recognizedData.answers.length == index + 1 ? recognizedData?.outputs : {}, + transactionId: recognizedData._id, + feedbackable: false + } + }, { + headers: { + Authorization: `Bearer ${settings_667ef98d3ee8930a5debcbdb.messenger.token}` + } + }); + } catch (e) {} + } + resolve(true); + }); + axios.post(`${settings_667ef98d3ee8930a5debcbdb.messenger.apiBaseURL}Event/isTyping`, { + workspaceId: req.params.workspaceId, + conversationId: sessionId, + userId: settings_667ef98d3ee8930a5debcbdb.messenger.participantId, + typing: false + }, { + headers: { + Authorization: `Bearer ${settings_667ef98d3ee8930a5debcbdb.messenger.token}` + } + }); + } + res.send(recognizedData); + } catch (error) { + console.log(error.message); + res.send({ + answers: ['Something went wrong. Please try again.'], + outputs: {} + }); + } +})(); \ No newline at end of file diff --git a/copilot/_studio_dependencies/ProxyScript/proactive_agent.js b/copilot/_studio_dependencies/ProxyScript/proactive_agent.js new file mode 100644 index 0000000..9b42612 --- /dev/null +++ b/copilot/_studio_dependencies/ProxyScript/proactive_agent.js @@ -0,0 +1,382 @@ +console.log({ query: req.query, params: req.params, body: req.body }); + +const msgrSettings = settings_667ef98d3ee8930a5debcbdb; +const paSettings = proactive_agent_settings; +const studioToken = msgrSettings + ? { + Authorization: `Bearer ${msgrSettings.messenger.token}`, + } + : {}; +const channel = req.body?.metadata?.channel; + +let bot_user = { + ...paSettings.messenger.user, +}; + +const app = { + routes: [], + route: function (regexp, fn) { + this.routes.push({ regexp: new RegExp(regexp), function: fn }); + }, + run: function (req, res) { + const path = + req.params[0].split( + `/ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/${req.params.route}` + )[1] || "/"; + + for (var route of this.routes) { + console.log(`Checking route: ${route.regexp}`); + console.log(`Against path: ${path}`); + if (route.regexp.test(path)) { + console.log(`Matched route: ${route.regexp}`); + route.function(req, res, route.regexp.exec(path)); + return; + } + } + // If no route matches, return a 404 status + const status = { path: req.params[0], status: 404 }; + res.status(404).send(status); + }, +}; + +const redisKey = (userId) => { + return `user-map-${userId}`; +}; + +const axioshelper = (message, config, callback, retries = 3, delay = 100) => { + console.log(config); + return new Promise(async (resolve, reject) => { + for (let i = 0; i < retries; i++) { + try { + const response = await axios(config); + + if (response.status < 200 || response.status > 299) { + if (callback) { + await callback(); + } else { + throw new Error( + `Axios Request failed! ${JSON.stringify(response)}` + ); + } + } + resolve(response); + break; + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log("Error", error.message); + } + console.log(error.config); + if (i === retries - 1) { + if (callback) { + await callback(); + } + console.log({ + retry: i + 1, + error: "out of retries", + message, + url: config.url, + }); + reject(error); + } else { + console.log({ + level: "warn", + message: `Attempt ${i + 1} failed.`, + url: config.url, + }); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + }); +}; + +async function createSessionMap(umStr, sessionMap) { + console.log({ method: "createSessionMap", sessionMap }); + let keys = Object.keys(sessionMap); + keys.forEach(async (key) => { + await redis.hSet(umStr, key, sessionMap[key]); + }); + await redis.expire(umStr, 3600); +} + +async function m2mAuthentication() { + let data = new URLSearchParams({ + grant_type: paSettings.copilot.oauth2.grant_type, + client_id: paSettings.copilot.oauth2.clientId, + client_secret: paSettings.copilot.oauth2.clientSecret, + audience: paSettings.copilot.oauth2.audience, + }); + + let token = await axioshelper("m2m authentication", { + method: "post", + url: paSettings.copilot.tokenUrl, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data, + }) + .then((response) => { + return response.data.access_token; + }) + .catch((error) => { + console.error("Error during M2M authentication:", error); + throw error; + }); + + return token; +} + +app.route("^(?:/userId/([^/]+))(?:/$)?$", async (req, res, match) => { + try { + console.log(`Received request for userId: ${match[1]}`); + const modelName = + req.params?.modelName || proactive_agent_settings.modelName; + if (match.length > 1) { + const userId = match[1]; + const smStr = redisKey(userId); + let sessionMap = await redis.hGetAll(smStr); + + if (!sessionMap || !sessionMap.conversationId) { + if (channel === "web") { + console.log(`No session found for userId: ${userId} for web channel`); + return res.status(404).send({ error: "Session not found" }); + } else if (channel === "copilot") { + // no session data is okay for copilot, create one now + sessionMap = { + customerId: paSettings.copilot.customerId, + upn: userId, + userUUID: req.body?.metadata?.userUUID, + }; + await createSessionMap(smStr, sessionMap); + } + } else { + console.log({ sessionMap }); + } + + const metadata = { + userId: bot_user.userId, + channel + }; + + const recognizedData = await axioshelper("run model", { + method: "post", + url: `${settings_667ef98d3ee8930a5debcbdb.nlu.apiBaseURL}Model/run/${req.params.workspaceId}/${req.params.branch}/${modelName}`, + headers: studioToken, + data: { + input: req.body.input, + conversationId: sessionMap.conversationId, + settings: settings_667ef98d3ee8930a5debcbdb.nlu.settings, + metadata: metadata, + }, + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + //If we have error handling turned off, don't record the error + if (settings_667ef98d3ee8930a5debcbdb.errorHandling === false) { + throw error; + } else { + // Conversation ID is generated by the NLU + // Must make sure Conversation ID is created properly for an error transaction + // Get it from session map if it exists, or generate it if we got the error at the very first transaction. + const conversationId = sessionMap.conversationId || uuidv4(); + let errorPayload = { + id: uuidv4(), + workspaceId: req.params.workspaceId, + errorInfo: { + name: error.name, + message: error.message, + stack: error.stack, + }, + conversationId, + input: req.body.input, + answers: [settings_667ef98d3ee8930a5debcbdb.responses.modelError], + req: { + params: req.params, + query: req.query, + body: req.body, + headers: req.headers, + }, + metadata, + classificationResults: [ + { + label: "Model Error", + value: 0.0, + }, + ], + nerResults: { + entities: [], + sourceEntities: [], + }, + // Reporting tags to match GlobalSupport function reporting + tag: "ERROR", + reporting: { + tag: "ERROR", + modelResponse: "ERROR", + }, + }; + + // Add model name + errorPayload.req.params.route = modelName; + + //add any other details you wish to see in transaction data + + return errorPayload; + } + }); + + if (recognizedData.conversationId) { + await redis.hSet( + smStr, + "conversationId", + recognizedData.conversationId + ); + await redis.expire(smStr, 3600); + if (sessionMap.conversationId) { + } else { + db.analytics.addConversation({ + id: recognizedData.conversationId, + }); + } + } + recognizedData._id = ( + await db.analytics.addTransaction(recognizedData) + ).insertedId; + + // Send Answers to Messenger + new Promise(async (resolve) => { + for (const [index, answer] of recognizedData.answers.entries()) { + if (channel === "web") { + await axioshelper("messenger create", { + method: "post", + url: `${settings_667ef98d3ee8930a5debcbdb.messenger.apiBaseURL}Event/create`, + headers: { + Authorization: `Bearer ${settings_667ef98d3ee8930a5debcbdb.messenger.token}`, + }, + data: { + conversationId: sessionMap.sessionId, + metadata: { + channel: "web", + }, + input: answer, + sentBy: bot_user, + ping: nanoid(), + sentAt: Date.now(), + options: recognizedData?.options, + metadata: { + outputs: + recognizedData.answers.length == index + 1 + ? recognizedData?.outputs + : {}, + transactionId: recognizedData._id, + feedbackable: false, + }, + }, + }); + } else if (channel === "copilot") { + let bearerAuth = await m2mAuthentication(); + if (!bearerAuth) { + throw Error("Unable to Authenticate M2M"); + } + + let body = answer; + + if (typeof answer === "string" || answer instanceof String) { + // TODO: convert to simple text reply object + body = { + source: "//wa/us-east-1/int/demo/plain", + type: "verint.ui_messages.text.v1", + id: "ca5057b8-2c1d-4758-89d6-0370d6f28cc2", + time: "2020-07-30T14:44:00+00:00", + data: { + version: "1.0.0", + format: "plain", + persona_icon: { + id: "Verint-Bot-Head", + color: "#007ACC", + }, + title_icon: { + id: "message-status-info", + color: "#FF5722", + }, + body: answer, + }, + }; + } + + await axioshelper("copilot cloud-event", { + method: "post", + url: `https://apigw.us-east-1.wrk-1.aws.hydra.verint.com/int/wa/v1/cloud-event`, + headers: { + Authorization: `Bearer ${bearerAuth}`, + }, + data: { + ...body, + ...sessionMap, + }, + }); + } + } + + resolve(true); + }); + res.send(recognizedData); + } else { + res.status(400).send({ error: "Bad Request" }); + } + } catch (error) { + console.error(error); + return res.status(500).send({ + status: 500, + }); + } +}); + +app.route("/", async (req, res) => { + const sessionId = req.body.event?.conversationId; + const metadata = { + ...req.body.event?.metadata, + userId: req.body.event?.sentBy?.userId, + }; + + try { + if (sessionId && metadata.userId) { + await createSessionMap(redisKey(metadata.userId), { sessionId }); + + await axioshelper("messenger join", { + method: "post", + url: `${msgrSettings.messenger.apiBaseURL}Conversation/join`, + headers: studioToken, + data: { + _id: sessionId, + participant: bot_user, + }, + }); + + res.send(); + } else { + res.send({ error: "sessionId or userId not found on event." }); + } + } catch (error) { + console.log(error); + res.send({ + answers: ["Something went wrong. Please try again."], + outputs: {}, + }); + } +}); + +app.run(req, res); diff --git a/copilot/copilot-extension/manifest.json b/copilot/copilot-extension/manifest.json index e2c057a..8909b68 100644 --- a/copilot/copilot-extension/manifest.json +++ b/copilot/copilot-extension/manifest.json @@ -9,7 +9,7 @@ "side_panel": { "default_path": "src/pages/side-panel/index.html" }, - "permissions": ["sidePanel", "tabs"], + "permissions": ["sidePanel", "tabs", "clipboardWrite", "clipboardRead"], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" },