without sentiment
20 |
22 |
23 | Conversation
24 |
25 | {/*
*/}
28 |
29 | {currentSessionUser ? (
30 | conversations
31 | .filter((msg) => msg.session_id === currentSessionUser.session_id)
32 | .map((message, index) => (
33 |
34 | ))
35 | ) : null}
36 |
37 |
38 | );
39 | };
40 |
41 | export default Conversation;
42 |
--------------------------------------------------------------------------------
/api-server/utils/helpers.js:
--------------------------------------------------------------------------------
1 |
2 | const isTrue = (val) => String(val).toLowerCase() === 'true'
3 | const transformBoolean = (val, defaultValue = true) => {
4 | if (val === undefined || val === null) {
5 | return defaultValue !== undefined ? defaultValue : null
6 | }
7 | return isTrue(val)
8 | }
9 | const transformEmpty = (val) => (!val ? null : val)
10 |
11 | const transformEmptyString = (val) => (!val ? '' : val)
12 |
13 | const transformEmptyJSON = (val) => (val === undefined || val === null ? null : val)
14 |
15 | const transformNumber = (val) => {
16 | if (!val || isNaN(val)) return null
17 | return Number(val)
18 | }
19 | const transformArray = (val) => {
20 | if (typeof val === 'string') {
21 | let result = val
22 | if (val.startsWith('{')) {
23 | result = result.substring(1)
24 | }
25 |
26 | if (val.endsWith('}')) {
27 | result = result.substring(0, result.length - 1)
28 | }
29 | return result
30 | .split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/) // convert a string to a list split by `,`. this takes into account commas inside quotes
31 | .map((x) => x.replace(/(^")|("$)/g, ''))
32 | .filter((x) => !!x)
33 | }
34 |
35 | return val
36 | }
37 |
38 | const concat = (x, y) => x.concat(y)
39 | const flatMap = (f, xs) => xs.map(f).reduce(concat, [])
40 |
41 | class MergeMap extends Map {
42 | set(key, value) {
43 | return super.set(key, Object.assign(this.get(key) || {}, value))
44 | }
45 | }
46 |
47 | module.exports = {
48 | flatMap,
49 | isTrue,
50 | transformBoolean,
51 | transformEmpty,
52 | transformEmptyString,
53 | transformEmptyJSON,
54 | transformNumber,
55 | transformArray,
56 | MergeMap,
57 | }
58 |
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/GenesysStreamingSessionState.js:
--------------------------------------------------------------------------------
1 |
2 | const StreamingSessionState = require('./StreamingSessionState');
3 |
4 | class GenesysStreamingSessionState extends StreamingSessionState {
5 |
6 | // Counts used to track message exchanges with Genesys
7 | clientSeq = 0;
8 | serverSeq = 0;
9 |
10 | // Counts used to track
11 | eventCount = 0;
12 |
13 | // Used for debugging
14 | receivedBufferCount = 0;
15 |
16 | // Used to track the state machine of the session.
17 | state = 'connected';
18 |
19 | constructor(message){
20 | super(message.id);
21 |
22 | this.organizationId = message.parameters.organizationId;
23 | this.conversationId = message.parameters.conversationId;
24 |
25 | this.agent_id = message.agent_id; // this is added in manually from the front-end UI for now to let us isolate the agent ID
26 |
27 | // Extract all the participaten information.
28 | // Currently, the Genesys AudioHook participant only ever represents the customer. The agent participant is not
29 | // currently being monitored by Genesys. This may change in the future.
30 | // Here is an example of a participant object:
31 | //
32 | // "participant": {
33 | // "id": "883efee8-3d6c-4537-b500-6d7ca4b92fa0",
34 | // "ani": "+1-555-555-1234",
35 | // "aniName": "John Doe",
36 | // "dnis": "+1-800-555-6789"
37 | // }
38 |
39 | this.participant = message.parameters.participant;
40 | }
41 | }
42 | module.exports = GenesysStreamingSessionState;
--------------------------------------------------------------------------------
/api-server/socketio/configurePool.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configured Pool for SocketIo Server
3 | * this pool only listens to messages from postgres pg notify
4 | * keep in mind that when clients send messages to server, it doesn't use the pg adapter, but rather directly on the socket.on events
5 | * the "notification" event is special, used by socket.io and the database is simulating a cross-server communication to deliver messages
6 | * @returns {any} pg pool
7 | */
8 |
9 | const { Pool } = require('pg')
10 | const debug = require('debug')('configurePool')
11 | const config = require('../utils/config')
12 |
13 | const rejectUnauthorized = config.SOCKETIO_TRUST_SELF_SIGNED === 'false'
14 |
15 | const pool = new Pool({
16 | connectionString: config.SOCKETIO_DB_URI,
17 | max: 4,
18 | ssl: {
19 | rejectUnauthorized: rejectUnauthorized,
20 | },
21 | })
22 |
23 | debug('Enabling Notification Service')
24 | // Add a listener for the 'error' event
25 | pool.on('error', (err, client) => {
26 | console.error('Unexpected error on idle client', err)
27 | })
28 |
29 | // Optionally, add a listener for the 'connect' event
30 | pool.on('connect', (client) => {
31 | // client.on('notification', (msg) => {
32 | // debug(msg.channel) // foo
33 | // debug(msg.payload) // bar!
34 | // debug(msg)
35 | // })
36 | debug('New client connected to the pool')
37 | })
38 |
39 | // // Optionally, add a listener for the 'acquire' event
40 | // pool.on('acquire', (client) => {
41 | // debug('Client acquired from the pool');
42 | // });
43 |
44 | // // Optionally, add a listener for the 'remove' event
45 | // pool.on('remove', (client) => {
46 | // debug('Client removed from the pool');
47 | // });
48 |
49 | module.exports = pool
50 |
--------------------------------------------------------------------------------
/wrapper-ui/landing/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | background-color: white;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | body {
10 | display: flex;
11 | justify-content: center;
12 | align-items: flex-start;
13 | }
14 |
15 | .main-container {
16 | flex: 1 1 100%;
17 | display: flex;
18 | flex-flow: column wrap;
19 | align-items: center;
20 | display: -moz-box;
21 | display: -ms-flexbox;
22 | display: -moz-flex;
23 | display: -webkit-flex;
24 | }
25 |
26 | .bottom-container {
27 | margin-top: 145px;
28 | display: flex;
29 | justify-content: center;
30 | }
31 |
32 | .flex-top {
33 | flex: 1;
34 | text-align: center;
35 | margin-top: 2.5%;
36 | }
37 |
38 | .flex-bottom {
39 | font-family: IBMPlexSans-Medium;
40 | font-size: 14px;
41 | color: #42535c;
42 | letter-spacing: 0;
43 | line-height: 20px;
44 | font-weight: normal;
45 | flex: none;
46 | margin-right: 5px;
47 | margin-left: 5px;
48 | }
49 |
50 | .text-div {
51 | font-family: IBMPlexSans-Light;
52 | font-size: 20px;
53 | color: #152935;
54 | letter-spacing: 0;
55 | text-align: center;
56 | line-height: 24px;
57 | flex: 1;
58 | margin-bottom: 59px;
59 | }
60 |
61 | .button {
62 | font-family: IBMPlexSans-Medium;
63 | font-size: 14px;
64 | color: #ffffff;
65 | letter-spacing: 0;
66 | text-align: center;
67 | background-color: #4178be;
68 | border: none;
69 | padding: 10px 40px;
70 | text-decoration: none;
71 | cursor: pointer;
72 | width: 158px;
73 | height: 40px;
74 | margin-bottom: 4px;
75 | }
76 |
77 | .logo-icon {
78 | display: block;
79 | margin-left: auto;
80 | margin-right: auto;
81 | width: 70px;
82 | height: 70px;
83 | margin-bottom: 41px;
84 | margin-top: 81px;
85 | }
86 |
--------------------------------------------------------------------------------
/celery/aan_extensions/SentimentAgent/sentiment.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | from ibm_watson import NaturalLanguageUnderstandingV1
5 | from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
6 | from ibm_watson.natural_language_understanding_v1 import Features, EmotionOptions
7 |
8 | # Initialize logging
9 | logging.basicConfig(level=logging.INFO)
10 |
11 | # Setup IBM Watson NLU service
12 | authenticator = IAMAuthenticator(os.environ.get('AAN_NLU_API_KEY'))
13 | natural_language_understanding = NaturalLanguageUnderstandingV1(
14 | version='2022-04-07',
15 | authenticator=authenticator
16 | )
17 | natural_language_understanding.set_service_url(os.environ.get('AAN_NLU_URL'))
18 |
19 | def assess_sentiment(session_id, transcript):
20 | """
21 | Analyze the sentiment of the most recent message in the transcript using IBM Watson Natural Language Understanding.
22 | :param session_id: The session ID for the current call.
23 | :param transcript: The full transcript of the call so far.
24 | :return: A float value representing the sentiment of the most recent message.
25 | """
26 | try:
27 | recent_message = transcript.split("\n")[-1] if "\n" in transcript else transcript
28 | if len(recent_message.split(" ")) < 3:
29 | return False
30 | response = natural_language_understanding.analyze(
31 | text=recent_message,
32 | features=Features(emotion=EmotionOptions(document=True))
33 | ).get_result()
34 | emotions = response['emotion']['document']['emotion']
35 | print(f"Emotions returned: {emotions}")
36 | return emotions
37 | except Exception as e:
38 | logging.error(f"Error during sentiment assessment for session {session_id}: {e}")
39 | return False
--------------------------------------------------------------------------------
/docker-compose.config.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | postgres:
4 | image: postgres:15
5 | restart: always
6 | volumes:
7 | - .db_data:/var/lib/postgresql/data
8 | environment:
9 | POSTGRES_PASSWORD: postgrespassword
10 | graphql-engine:
11 | image: hasura/graphql-engine:v2.38.0
12 | ports:
13 | - "8080:8080"
14 | restart: always
15 | environment:
16 | ## postgres database to store Hasura metadata
17 | HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
18 | ## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
19 | PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
20 | ## enable the console served by server
21 | HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
22 | ## enable debugging mode. It is recommended to disable this in production
23 | HASURA_GRAPHQL_DEV_MODE: "true"
24 | HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
25 | ## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
26 | # HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
27 | ## uncomment next line to set an admin secret
28 | # HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
29 | HASURA_GRAPHQL_METADATA_DEFAULTS: '{"backend_configs":{"dataconnector":{"athena":{"uri":"http://data-connector-agent:8081/api/v1/athena"},"mariadb":{"uri":"http://data-connector-agent:8081/api/v1/mariadb"},"mysql8":{"uri":"http://data-connector-agent:8081/api/v1/mysql"},"oracle":{"uri":"http://data-connector-agent:8081/api/v1/oracle"},"snowflake":{"uri":"http://data-connector-agent:8081/api/v1/snowflake"}}}}'
30 |
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/SiprecStreamingSessionState.js:
--------------------------------------------------------------------------------
1 |
2 | const StreamingSessionState = require('./StreamingSessionState');
3 |
4 | class SiprecStreamingSessionState extends StreamingSessionState {
5 |
6 | // Counts used to track message exchanges with Genesys
7 | clientSeq = 0;
8 | serverSeq = 0;
9 |
10 | // Counts used to track
11 | eventCount = 0;
12 |
13 | // Used for debugging
14 | receivedBufferCount = 0;
15 |
16 | // Used to track the state machine of the session.
17 | state = 'connected';
18 |
19 | constructor(message){
20 | super(message.vgw-session-id);
21 |
22 | this.organizationId = message.parameters.organizationId;
23 | this.conversationId = message.parameters.conversationId;
24 |
25 | // Extract all the participaten information.
26 | // Currently, the Genesys AudioHook participant only ever represents the customer. The agent participant is not
27 | // currently being monitored by Genesys. This may change in the future.
28 | // Here is an example of a participant object:
29 | //
30 | // "participant": {
31 | // "id": "883efee8-3d6c-4537-b500-6d7ca4b92fa0",
32 | // "ani": "+1-555-555-1234",
33 | // "aniName": "John Doe",
34 | // "dnis": "+1-800-555-6789"
35 | // }
36 |
37 | if (message.parameters.participant){
38 | this.participant = message.parameters.participant;
39 | }
40 | else{
41 | // If there is no participant in the open message we set to not applicable.
42 | this.participant = {};
43 | this.participant.ani = 'n/a';
44 | this.participant.aniName = 'n/a';
45 | this.participant.dnis = 'n/a';
46 | }
47 |
48 | }
49 | }
50 | module.exports = SiprecStreamingSessionState;
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/MonoChannelStreamingSessionState.js:
--------------------------------------------------------------------------------
1 |
2 | const StreamingSessionState = require('./StreamingSessionState');
3 |
4 | class MonoChannelStreamingSessionState extends StreamingSessionState {
5 |
6 | // Counts used to track message exchanges with Genesys
7 | clientSeq = 0;
8 | serverSeq = 0;
9 |
10 | // Counts used to track
11 | eventCount = 0;
12 |
13 | // Used for debugging
14 | receivedBufferCount = 0;
15 |
16 | // Used to track the state machine of the session.
17 | state = 'connected';
18 |
19 | constructor(message){
20 | super(message.id);
21 |
22 | this.organizationId = message.parameters.organizationId;
23 | this.conversationId = message.parameters.conversationId;
24 |
25 | // Extract all the participaten information.
26 | // Currently, the Genesys AudioHook participant only ever represents the customer. The agent participant is not
27 | // currently being monitored by Genesys. This may change in the future.
28 | // Here is an example of a participant object:
29 | //
30 | // "participant": {
31 | // "id": "883efee8-3d6c-4537-b500-6d7ca4b92fa0",
32 | // "ani": "+1-555-555-1234",
33 | // "aniName": "John Doe",
34 | // "dnis": "+1-800-555-6789"
35 | // }
36 |
37 | if (message.parameters.participant){
38 | this.participant = message.parameters.participant;
39 | }
40 | else{
41 | // If there is no participant in the open message we set to not applicable.
42 | this.participant = {};
43 | this.participant.ani = 'n/a';
44 | this.participant.aniName = 'n/a';
45 | this.participant.dnis = 'n/a';
46 | }
47 |
48 | }
49 | }
50 | module.exports = MonoChannelStreamingSessionState;
--------------------------------------------------------------------------------
/celery/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.9.3
2 | aiosignal==1.3.1
3 | amqp==5.2.0
4 | attrs==23.2.0
5 | bidict==0.23.1
6 | billiard==4.2.0
7 | celery==5.3.6
8 | certifi==2024.2.2
9 | charset-normalizer==3.3.2
10 | click==8.1.7
11 | click-didyoumean==0.3.0
12 | click-plugins==1.1.1
13 | click-repl==0.3.0
14 | Deprecated==1.2.14
15 | dnspython==2.6.1
16 | eventlet==0.36.1
17 | flower==2.0.1
18 | frozenlist==1.4.1
19 | googleapis-common-protos==1.59.1
20 | greenlet==3.0.3
21 | grpcio==1.62.1
22 | h11==0.14.0
23 | humanize==4.9.0
24 | ibm-cloud-sdk-core==3.20.0
25 | ibm-cos-sdk==2.13.4
26 | ibm-cos-sdk-core==2.13.4
27 | ibm-cos-sdk-s3transfer==2.13.4
28 | ibm-watson==8.0.0
29 | ibm_watson_machine_learning==1.0.356
30 | idna==3.6
31 | importlib-metadata==7.0.0
32 | jmespath==1.0.1
33 | joblib==1.4.2
34 | kombu==5.3.5
35 | lomond==0.3.3
36 | multidict==6.0.5
37 | nltk==3.8.1
38 | numpy==1.26.4
39 | opentelemetry-api==1.24.0
40 | opentelemetry-distro==0.45b0
41 | opentelemetry-exporter-jaeger==1.21.0
42 | opentelemetry-exporter-jaeger-proto-grpc==1.21.0
43 | opentelemetry-exporter-jaeger-thrift==1.21.0
44 | opentelemetry-instrumentation==0.45b0
45 | opentelemetry-instrumentation-celery==0.45b0
46 | opentelemetry-instrumentation-redis==0.45b0
47 | opentelemetry-sdk==1.24.0
48 | opentelemetry-semantic-conventions==0.45b0
49 | packaging==24.0
50 | pandas==2.1.4
51 | prometheus_client==0.20.0
52 | prompt-toolkit==3.0.43
53 | protobuf==4.25.3
54 | PyJWT==2.8.0
55 | python-dateutil==2.8.2
56 | python-dotenv==1.0.1
57 | python-engineio==4.9.0
58 | python-socketio==5.11.2
59 | pytz==2024.1
60 | redis==5.0.1
61 | regex==2024.5.15
62 | requests==2.31.0
63 | scikit-learn==1.4.2
64 | scipy==1.13.0
65 | simple-websocket==1.0.0
66 | six==1.16.0
67 | tabulate==0.9.0
68 | threadpoolctl==3.5.0
69 | thrift==0.20.0
70 | tornado==6.4
71 | tqdm==4.66.4
72 | typing_extensions==4.11.0
73 | tzdata==2024.1
74 | urllib3==2.1.0
75 | vine==5.1.0
76 | wcwidth==0.2.13
77 | websocket-client==1.7.0
78 | wrapt==1.16.0
79 | wsproto==1.2.0
80 | yarl==1.9.4
81 | zipp==3.18.1
82 |
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Dashboard/FileStreamer/FileStreamer.jsx:
--------------------------------------------------------------------------------
1 | import {send} from "../../../libs/WAVStreamer";
2 | import {togglePauseResume} from "../../../libs/WAVStreamer";
3 | import React, { useState } from 'react';
4 | import { useAgentId } from "../../../hooks/useAgentIdProvider";
5 |
6 | const FileStreamer = () => {
7 | const [isPlaying, setIsPlaying] = useState(false);
8 | const [audioStarted, setAudioStarted] = useState(false);
9 | const { agentId } = useAgentId()
10 |
11 | const handleFileChange = (event) => {
12 | const selectedFile = event.target.files[0];
13 |
14 | if (!selectedFile){
15 | alert ('Please select a file first.');
16 | return;
17 | }
18 |
19 | const fileReader = new FileReader();
20 |
21 | // Set up event handlers for the FileReader
22 | fileReader.onload = function(event) {
23 | const fileData = event.target.result;
24 | send(fileData, agentId);
25 | setIsPlaying(!isPlaying);
26 | setAudioStarted(true);
27 | }
28 |
29 | // Read the file as ArrayBuffer
30 | fileReader.readAsArrayBuffer(selectedFile);
31 | }
32 |
33 | const togglePlayPause = () => {
34 | togglePauseResume();
35 | setIsPlaying(!isPlaying);
36 | }
37 |
38 | return (
39 |
40 |
41 |
42 |
43 | {isPlaying ? 'Pause' : ' Play'}
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 |
54 | export default FileStreamer;
55 |
--------------------------------------------------------------------------------
/wrapper-ui/server/utils/listBuckets.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const IBM = require("ibm-cos-sdk");
4 | require("dotenv").config();
5 |
6 | var config = {
7 | endpoint: process.env.COS_ENDPOINT,
8 | apiKeyId: process.env.COS_API_KEY_ID,
9 | serviceInstanceId: process.env.COS_SERVICE_INSTANCE_ID,
10 | signatureVersion: process.env.COS_SIGNATURE_VERSION,
11 | };
12 |
13 | var cos = new IBM.S3(config);
14 |
15 | router.get("/", async (req, res) => {
16 | const bucketName = process.env.BUCKET_NAME;
17 |
18 | try {
19 | // List all objects in the bucket
20 | const listObjectsResponse = await cos
21 | .listObjects({ Bucket: bucketName })
22 | .promise();
23 |
24 | // Array to store content of each object
25 | const objectsContent = [];
26 |
27 | // Iterate over each object and fetch its content
28 | for (const object of listObjectsResponse.Contents) {
29 | const objectKey = object.Key;
30 |
31 | // Get the content of each object
32 | const getObjectResponse = await cos
33 | .getObject({ Bucket: bucketName, Key: objectKey })
34 | .promise();
35 |
36 | //console.log("File COntent", getObjectResponse.Body)
37 |
38 | // Extract relevant information for each object
39 | const objectInfo = {
40 | Key: objectKey,
41 | Size: getObjectResponse.ContentLength,
42 | ContentType: getObjectResponse.ContentType,
43 | LastModified: getObjectResponse.LastModified,
44 | //Body: getObjectResponse.Body
45 | // Add any other relevant information you want to include
46 | };
47 |
48 | objectsContent.push(objectInfo);
49 |
50 | console.log(objectInfo);
51 | }
52 |
53 | // Send the content of all objects as the response
54 | res.status(200).json({ bucketName, objectsContent });
55 | } catch (error) {
56 | console.error(`ERROR: ${error.code} - ${error.message}`);
57 | res
58 | .status(500)
59 | .json({ error: `Error retrieving objects content: ${error.message}` });
60 | }
61 | });
62 |
63 | module.exports = router;
64 |
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Dashboard/WatsonAssistant/WatsonAssistant.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | const WatsonAssistant = () => {
4 | const chatContainerRef = useRef(null);
5 | const scriptAddedRef = useRef(false);
6 |
7 | useEffect(() => {
8 | if (!scriptAddedRef.current) {
9 | scriptAddedRef.current = true;
10 |
11 | window.watsonAssistantChatOptions = {
12 | integrationID: process.env.VITE_WA_INTEGRATION_ID, // The ID of this integration.
13 | region: process.env.VITE_WA_REGION, // The region your integration is hosted in.
14 | serviceInstanceID: process.env.VITE_WA_SERVICE_INSTANCE_ID, // The ID of your service instance.
15 | showLauncher: false,
16 | showRestartButton: true,
17 | disableSessionHistory: true,
18 | element: chatContainerRef.current,
19 | onLoad: function (instance) {
20 | window.watsonAssistantChatInstance = instance;
21 |
22 | // Disable the Home Screen
23 | instance.updateHomeScreenConfig({
24 | is_on: false
25 | });
26 |
27 | // Restart the conversation on startup
28 | console.log("Restarting watson assistand conversations.");
29 | instance.restartConversation();
30 |
31 | instance.render().then(() => {
32 | if (!instance.getState().isWebChatOpen) {
33 | instance.openWindow();
34 | }
35 | });
36 | },
37 | };
38 |
39 | const t = document.createElement("script");
40 | t.src =
41 | "https://web-chat.global.assistant.watson.appdomain.cloud/versions/" +
42 | (window.watsonAssistantChatOptions.clientVersion || "latest") +
43 | "/WatsonAssistantChatEntry.js";
44 | document.head.appendChild(t);
45 | }
46 | }, []);
47 |
48 | return (
49 |
55 | );
56 | };
57 |
58 | export default WatsonAssistant;
59 |
--------------------------------------------------------------------------------
/wrapper-ui/server/app-basic.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const fs = require("fs")
3 | const path = require("path");
4 | const app = express();
5 | const { createProxyMiddleware } = require('http-proxy-middleware')
6 | let port;
7 |
8 | if (process.env.NODE_ENV === undefined) {
9 | port = 3003;
10 | } else {
11 | port = 8080;
12 | }
13 |
14 | app.use(express.json());
15 | const apiServerProxy = createProxyMiddleware({
16 | target: `${process.env.ANN_SOCKETIO_SERVER}/socket.io/'`,
17 | changeOrigin: true,
18 | ws: true, // we turn this off so that the default upgrade function doesn't get called, we call our own later server.on('upgrade)
19 | }
20 | )
21 |
22 | app.use('/socket.io', apiServerProxy)
23 |
24 |
25 | console.log(`agent dashboard ${process.env.ANN_AGENT_DASHBOARD}`)
26 | console.log(`${process.env.ANN_AGENT_DASHBOARD}`)
27 | const agentDashboardProxy = createProxyMiddleware({
28 | target: `${process.env.ANN_AGENT_DASHBOARD}`,
29 | changeOrigin: true,
30 | }
31 | )
32 |
33 | app.use('/agent', agentDashboardProxy)
34 |
35 | // Middleware to inject proxyPath into the HTML file
36 | app.get('/', (req, res, next) => {
37 | const indexPath = path.resolve(__dirname, '../dist/index.html');
38 |
39 | fs.readFile(indexPath, 'utf-8', (err, html) => {
40 | if (err) {
41 | return next(err);
42 | }
43 |
44 | // Inject the proxy path into the HTML (via a global JS variable)
45 | const modifiedHtml = html.replace(
46 | '',
47 | ``
48 | );
49 |
50 | res.send(modifiedHtml);
51 | });
52 | });
53 |
54 |
55 | // app.use("*", express.static(path.join(__dirname, "../dist")));
56 |
57 | // static files
58 | app.use(express.static(path.join(__dirname, "../dist")))
59 |
60 | // all other requests, serve index.html
61 | app.get('/*', (req, res) => {
62 | res.sendFile(path.join(__dirname, "../dist", 'index.html'))
63 | })
64 |
65 | app.get("/healthcheck", (req, res) => {
66 | res.send("Healthy!");
67 | });
68 |
69 | app.listen(port, () => {
70 | console.log(`Example app listening on port ${port}`);
71 | });
72 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | .idea
--------------------------------------------------------------------------------
/wrapper-ui/server/utils/App-ID.html:
--------------------------------------------------------------------------------
1 | !DOCTYPE html>
2 |
3 |
4 |
Sample App
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
32 |
33 |
48 |
49 |
55 |
56 |
57 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/celery/celery_worker.py:
--------------------------------------------------------------------------------
1 | from opentelemetry.instrumentation.celery import CeleryInstrumentor
2 | from opentelemetry.exporter.jaeger.thrift import JaegerExporter
3 | from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
4 | from opentelemetry import trace
5 | from opentelemetry.sdk.trace import TracerProvider
6 | import logging
7 |
8 | from celery import Celery
9 | from celery.signals import worker_process_init
10 |
11 | from dotenv import load_dotenv
12 | import os
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | DISABLE_TRACING = os.getenv('DISABLE_TRACING', False) == 'true'
17 | TRACING_COLLECTOR_ENDPOINT = os.getenv('TRACING_COLLECTOR_ENDPOINT', 'jaeger')
18 | TRACING_COLLECTOR_PORT = os.getenv('TRACING_COLLECTOR_PORT', '14268')
19 |
20 |
21 | @worker_process_init.connect(weak=False)
22 | def init_celery_tracing(*args, **kwargs):
23 | if os.getenv("TELEMETRY", ''):
24 | CeleryInstrumentor().instrument()
25 | print("CeleryInstrumentation Enabled")
26 | trace.set_tracer_provider(TracerProvider())
27 |
28 | if DISABLE_TRACING:
29 | span_processor = BatchSpanProcessor(ConsoleSpanExporter())
30 | else:
31 | print("JaegerExporter Enabled")
32 | jaeger_exporter = JaegerExporter(
33 | collector_endpoint=f'http://{TRACING_COLLECTOR_ENDPOINT}:{TRACING_COLLECTOR_PORT}/api/traces?format=jaeger.thrift',
34 | )
35 | span_processor = BatchSpanProcessor(jaeger_exporter)
36 |
37 | trace.get_tracer_provider().add_span_processor(span_processor)
38 |
39 | app = Celery(
40 | "agent_assist_neo",
41 | broker=os.getenv("AAN_CELERY_BROKER_URI", "amqp://admin:adminpass@localhost:5672"),
42 | # backend=os.getenv("AAN_CELERY_BACKEND_URI", "redis://localhost:6379/1"),
43 | include=['aan_extensions.TranscriptionAgent.tasks',
44 | 'aan_extensions.DispatcherAgent.tasks',
45 | 'aan_extensions.ExtractionAgent.tasks',
46 | 'aan_extensions.NextBestActionAgent.tasks',
47 | 'aan_extensions.CacheAgent.tasks',
48 | 'aan_extensions.SummaryAgent.tasks',
49 | 'aan_extensions.SentimentAgent.tasks',
50 | ]
51 | )
52 |
53 | if __name__ == '__main__':
54 | app.start()
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/client/components/WatsonxAssistant/WatsonxAssistant.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import * as styles from "./WatsonxAssistant.module.scss";
3 | import * as widgetStyles from "@client/widget.module.scss";
4 | import {useEnvVars} from "@client/providers/EnvVars"
5 |
6 | const WatsonxAssistant = () => {
7 | const chatContainerRef = useRef(null);
8 | const scriptAddedRef = useRef(false);
9 | const envVars:any = useEnvVars();
10 |
11 | useEffect(() => {
12 | if (envVars.waIntegrationId && !scriptAddedRef.current) {
13 | scriptAddedRef.current = true;
14 | window.watsonAssistantChatOptions = {
15 | integrationID: envVars.waIntegrationId, // The ID of this integration.
16 | region: envVars.waRegion, // The region your integration is hosted in.
17 | serviceInstanceID: envVars.waServiceInstanceId, // The ID of your service instance.
18 | showLauncher: false,
19 | showRestartButton: true,
20 | disableSessionHistory: true,
21 | element: chatContainerRef.current,
22 | onLoad: function (instance: any) {
23 | window.watsonAssistantChatInstance = instance;
24 |
25 | // Disable the Home Screen
26 | instance.updateHomeScreenConfig({
27 | is_on: false
28 | });
29 |
30 | // Restart the conversation on startup
31 | console.log("Restarting watson assistand conversations.");
32 | instance.restartConversation();
33 |
34 | instance.render().then(() => {
35 | if (!instance.getState().isWebChatOpen) {
36 | instance.openWindow();
37 | }
38 | });
39 | },
40 | };
41 |
42 | const t = document.createElement("script");
43 | t.src =
44 | "https://web-chat.global.assistant.watson.appdomain.cloud/versions/latest/WatsonAssistantChatEntry.js";
45 | document.head.appendChild(t);
46 | }
47 | }, [envVars]);
48 |
49 | return (
50 |
56 | );
57 | };
58 |
59 | export default WatsonxAssistant;
60 |
--------------------------------------------------------------------------------
/celery/aan_extensions/TranscriptionAgent/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 | from celery_worker import app
3 | from BaseAgent import BaseTask
4 | import logging
5 | import json
6 |
7 | from opentelemetry import trace
8 | from opentelemetry.trace import SpanKind
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | class colors:
13 | OKGREEN = '\033[92m'
14 | OKBLUE = '\033[94m'
15 | ENDC = '\033[0m'
16 |
17 | @app.task(base=BaseTask.BaseTask, bind=True)
18 | def process_transcript(self,topic, message):
19 | with trace.get_tracer(__name__).start_as_current_span("process_transcript", kind=SpanKind.PRODUCER) as span:
20 | result = topic + '---' + message
21 | print(f"TranscriptAgent {colors.OKGREEN}{topic}{colors.ENDC} + {colors.OKBLUE}{message}{colors.ENDC}")
22 | # print(self.sio)
23 | message_headers = process_transcript.request.headers
24 |
25 | # Extract baggage items from message headers
26 | # Seems like the node app isn't sending any baggage properly from the auto instrumentation
27 | baggage = {}
28 | print(message_headers)
29 | if message_headers is not None and not message_headers:
30 | for key, value in message_headers.items():
31 | logger.debug(f"headers: {key}={value}")
32 | print(f"headers: {key}={value}")
33 | if key.startswith('baggage-'):
34 | baggage[key[len('baggage-'):]] = value
35 |
36 | # Process baggage items as needed
37 | for key, value in baggage.items():
38 | logger.debug(f"Baggage: {key}={value}")
39 | message_data = json.loads(message)
40 | try:
41 | with trace.get_tracer(__name__).start_as_current_span("emit_socketio") as child_span:
42 | #self.await_sio_emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery')
43 |
44 | self.sio.emit('celeryMessage', {'payloadString': message, 'destinationName': topic, 'agent_id': message_data['agent_id']}, namespace='/celery')
45 | # self.redis_client.append_to_list_json()
46 | except Exception as e:
47 | print(e)
48 | # the return result is stored in the celery backend
49 | return result
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/CeleryEventPublisher.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const celery = require('celery-node');
4 |
5 | const LOG_LEVEL = process.env.LOG_LEVEL;
6 | const logger = require('pino')({ level: LOG_LEVEL, name: 'CeleryEventPublisher' });
7 |
8 | const { trace, SpanKind, context } = require('@opentelemetry/api');
9 |
10 | const tracer = trace.getTracer('GenesysAudioHookAdapter');
11 |
12 | const EventPublisher = require('./EventPublisher');
13 |
14 | class CeleryEventPublisher extends EventPublisher {
15 |
16 | constructor() {
17 | super();
18 |
19 | // TODO: take proper env vars
20 | const rabbitUrl = process.env.AAN_AMQP_URI || 'amqp://admin:adminpass@localhost:5672';
21 | const redisUrl = process.env.AAN_REDIS_URI || 'redis://localhost:6379/1'
22 |
23 | this.client = celery.createClient(
24 | rabbitUrl, redisUrl
25 | );
26 |
27 | //this.client.conf.TASK_PROTOCOL = 1
28 |
29 | // name of the celery task
30 | this.task = this.client.createTask("aan_extensions.DispatcherAgent.tasks.process_transcript");
31 | logger.debug('CeleryEventPublisher: established celery client');
32 | return this;
33 | }
34 |
35 | /* eslint-disable class-methods-use-this */
36 | publish(topic, message, parentSpanCtx) {
37 | logger.debug('CeleryEventPublisher: publishing message: ' + message + ' on topic: ' + topic);
38 | // mqttClient.publish(topic, message);
39 | // const span = tracer.startSpan('CeleryEventPublisher', parentSpanCtx)
40 | // this.task.applyAsync([topic, message])
41 | // span.end()
42 | const execTask = this.task.applyAsync
43 | const taskInput = [topic, message]
44 | tracer.startActiveSpan('CeleryEventPublisher.send_celery', {kind: SpanKind.PRODUCER} ,parentSpanCtx, (span) => {
45 | logger.debug('send_celery context ')
46 | logger.debug(parentSpanCtx)
47 | logger.debug(span._spanContext)
48 | logger.debug(span.parentSpanId)
49 | //console.log(execTask)
50 | //context.with(parentSpanCtx, execTask, this, taskInput)
51 | this.task.applyAsync([topic, message])
52 | span.end();
53 | });
54 |
55 | }
56 |
57 | destroy() {
58 | // Force the shutdown of the client connection.
59 | this.client.disconnect()
60 | }
61 | }
62 | module.exports = CeleryEventPublisher;
63 |
--------------------------------------------------------------------------------
/wrapper-ui/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | // eslint-disable-next-line no-unused-vars
6 | export default defineConfig(({ command, mode }) => {
7 | const env = loadEnv(mode, process.cwd(), "");
8 |
9 | return {
10 | define: {
11 | "process.env.VITE_MQTT_CALLER_ID": JSON.stringify(
12 | env.VITE_MQTT_CALLER_ID,
13 | ),
14 | "process.env.VITE_WA_INTEGRATION_ID": JSON.stringify(
15 | env.VITE_WA_INTEGRATION_ID,
16 | ),
17 | "process.env.VITE_WA_REGION": JSON.stringify(env.VITE_WA_REGION),
18 | "process.env.VITE_WA_SERVICE_INSTANCE_ID": JSON.stringify(
19 | env.VITE_WA_SERVICE_INSTANCE_ID,
20 | ),
21 | "process.env.VITE_LOCAL_MICROSERVICES": JSON.stringify(
22 | env.VITE_LOCAL_MICROSERVICES,
23 | ),
24 | // "process.env.YOUR_BOOLEAN_VARIABLE": env.YOUR_BOOLEAN_VARIABLE,
25 | // If you want to exposes all env variables, which is not recommended
26 | // 'process.env': env
27 | },
28 | plugins: [react()],
29 | optimizeDeps: {
30 | esbuildOptions: {
31 | // Node.js global to browser globalThis
32 | define: {
33 | global: "globalThis",
34 | },
35 | },
36 | },
37 | build: {
38 | minify: false,
39 |
40 | //experienced build issues with this rollup options
41 | // rollupOptions: {
42 | // output: {
43 | // manualChunks(id) {
44 | // if (id.includes("node_modules")) {
45 | // return id
46 | // .toString()
47 | // .split("node_modules/")[1]
48 | // .split("/")[0]
49 | // .toString();
50 | // }
51 | // },
52 | // },
53 | // },
54 | },
55 | // take note! in dev mode, vite config proxies to localhost 8000 (api-server)
56 | server: {
57 | open: "/protected",
58 | proxy: {
59 | '/socket.io': {
60 | target: 'ws://localhost:8000',
61 | changeOrigin: true,
62 | ws: true,
63 | },
64 | '/agent' : {
65 | target: 'http://localhost:3000',
66 | changeOrigin: true,
67 | },
68 | },
69 | },
70 | };
71 | });
72 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/server/index.ts:
--------------------------------------------------------------------------------
1 |
2 | import { readFile } from "fs";
3 | import path from "path";
4 | import express, {Express, Router} from "express";
5 | import helmet from "helmet";
6 | import {createServer} from "http";
7 | import setupWebsockets from "@server/websockets";
8 | import setupTelemetry from "@server/telemetry";
9 | import setupEnvironment from "@server/routes/environment";
10 | import setupAuth from "@server/auth";
11 |
12 | const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
13 | const ENABLE_AUTH = process.env.ENABLE_AUTH === "true";
14 | const ANN_WRAPPER_DASHBOARD = process.env.ANN_WRAPPER_DASHBOARD || 'http://localhost:3003'
15 | setupTelemetry();
16 |
17 | const app: Express = express();
18 | const router = Router();
19 | const server = createServer(app);
20 |
21 | app.use(helmet({
22 | contentSecurityPolicy: {
23 | directives: {
24 | "img-src": ["'self'", "https: data:"],
25 | "default-src": ["'self'"],
26 | "connect-src": ["'self'", "http://localhost:5173", "http://localhost", "https://*.watson.appdomain.cloud", `${ANN_WRAPPER_DASHBOARD}`],
27 | "frame-ancestors": ["'self'", "http://localhost:5173", "http://localhost", "https://*.watson.appdomain.cloud", `${ANN_WRAPPER_DASHBOARD}`],
28 | "script-src": ["'self'", "'unsafe-eval'", "'unsafe-inline'", "http://localhost:5173", "http://localhost", "https://*.watson.appdomain.cloud", `${ANN_WRAPPER_DASHBOARD}`],
29 | "style-src": ["'self'", "'unsafe-eval'", "'unsafe-inline'", "http://localhost:5173", "http://localhost", "https://*.watson.appdomain.cloud", `${ANN_WRAPPER_DASHBOARD}`],
30 | }
31 | }
32 | }));
33 |
34 | if (ENABLE_AUTH) {
35 | setupAuth(app);
36 | }
37 |
38 |
39 | app.use("/", express.static("./dist/client"));
40 |
41 | setupWebsockets(app);
42 | setupEnvironment(router);
43 |
44 | app.use("/api", router);
45 |
46 | server.listen(PORT, () =>
47 | console.log(`[server]: Server is running at http://localhost:${PORT}`)
48 | );
49 |
50 | /*
51 | if (true) {
52 | const socketServer = createServer();
53 | const io = new Server(socketServer);
54 |
55 | io.on('connection', (socket) => {
56 | socket.on('join', function (room) {
57 | socket.join(room);
58 | });
59 | });
60 |
61 | socketServer.listen(8000, () =>
62 | console.log(`[server]: Socket.io server is running at http://localhost:8000`)
63 | );
64 | }*/
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .pgdata/
3 | .tmp/
4 | .DS_Store
5 |
6 | values.yaml
7 | values.dev.yaml
8 | values.*.yaml
9 |
10 | # ssl test docker
11 | docker-compose.ssl.yaml
12 | .ci/debug.sh
13 | cacert.pem
14 |
15 | .npmrc
16 |
17 | .schemaspy
18 | __pycache__
19 |
20 | .vscode/diff/*.txt
21 | .vscode/diff/diff-master.json
22 |
23 | # Created by https://www.gitignore.io/api/node
24 | # Edit at https://www.gitignore.io/?templates=node
25 |
26 | ### Node ###
27 | # Logs
28 | logs
29 | *.log
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 | lerna-debug.log*
34 |
35 | # Diagnostic reports (https://nodejs.org/api/report.html)
36 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
37 |
38 | # Runtime data
39 | pids
40 | *.pid
41 | *.seed
42 | *.pid.lock
43 |
44 | # Directory for instrumented libs generated by jscoverage/JSCover
45 | lib-cov
46 |
47 | # Coverage directory used by tools like istanbul
48 | coverage
49 | *.lcov
50 |
51 | # nyc test coverage
52 | .nyc_output
53 |
54 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
55 | .grunt
56 |
57 | # Bower dependency directory (https://bower.io/)
58 | bower_components
59 |
60 | # node-waf configuration
61 | .lock-wscript
62 |
63 | # Compiled binary addons (https://nodejs.org/api/addons.html)
64 | build/Release
65 |
66 | # Dependency directories
67 | node_modules/
68 | jspm_packages/
69 |
70 | # TypeScript v1 declaration files
71 | typings/
72 |
73 | # TypeScript cache
74 | *.tsbuildinfo
75 |
76 | # Optional npm cache directory
77 | .npm
78 |
79 | # Optional eslint cache
80 | .eslintcache
81 |
82 | # Optional REPL history
83 | .node_repl_history
84 |
85 | # Output of 'npm pack'
86 | *.tgz
87 |
88 | # Yarn Integrity file
89 | .yarn-integrity
90 |
91 | # dotenv environment variables file
92 | .env
93 | .env.test
94 | .env.local
95 | .env.k8
96 | .env.*
97 | !.env.example
98 |
99 | # parcel-bundler cache (https://parceljs.org/)
100 | .cache
101 |
102 | # next.js build output
103 | .next
104 |
105 | # nuxt.js build output
106 | .nuxt
107 |
108 | # vuepress build output
109 | .vuepress/dist
110 |
111 | # Serverless directories
112 | .serverless/
113 |
114 | # FuseBox cache
115 | .fusebox/
116 |
117 | # DynamoDB Local files
118 | .dynamodb/
119 |
120 | # End of https://www.gitignore.io/api/node
121 | .idea
122 |
123 | .minio_storage
--------------------------------------------------------------------------------
/api-server/utils/data.js:
--------------------------------------------------------------------------------
1 | const df = require('dataframe-js')
2 | const HashMap = require('hashmap')
3 |
4 | function produceMapping(dataframe, columnName, dbMethod, headers) {
5 | const distinctValues = dataframe.distinct(columnName).toArray()
6 | return dbMethod(distinctValues, headers)
7 | }
8 |
9 | function createHashMap(data) {
10 | const output = []
11 | const map = data.map((x) => {
12 | if ('name' in x) {
13 | output.push([x.name, x.id])
14 | }
15 | })
16 | return new HashMap(output)
17 | }
18 |
19 | function produceMappers(data, requiredColumns, dbMappers, headers) {
20 | const maps = requiredColumns.map((colName, idx) => {
21 | return produceMapping(data, colName, dbMappers[idx], headers)
22 | })
23 | return Promise.all(maps).then((mappings) => {
24 | return mappings.map((mapData) => createHashMap(mapData))
25 | })
26 | }
27 |
28 | function mapNamesToID(data, requiredColumns, renamedColumns, hashMaps) {
29 | // https://stackoverflow.com/questions/46951390/dynamically-chain-methods-to-javascript-function
30 | // dataframe's map can only be called one at a time
31 | var dataFrame = data
32 | requiredColumns.map((colName, idx) => {
33 | dataFrame = dataFrame.map((row) => row.set(renamedColumns[idx], hashMaps[idx].get(row.get(colName))))
34 | })
35 | return dataFrame
36 | }
37 |
38 | function filterReqColumns(data, requiredColumns) {
39 | var dataFrame = data
40 | requiredColumns.map((colName, idx) => {
41 | dataFrame = dataFrame.filter((row) => row.get(colName) > 0)
42 | })
43 | return dataFrame
44 | }
45 |
46 | function renameColumns(data, requiredColumns, renamedColumns) {
47 | var dataFrame = data
48 | requiredColumns.map((colName, idx) => {
49 | dataFrame = dataFrame.rename(colName, renamedColumns[idx])
50 | })
51 | return dataFrame
52 | }
53 |
54 | function dropColumns(data, requiredColumns) {
55 | var dataFrame = data
56 | requiredColumns.map((colName, idx) => {
57 | dataFrame = dataFrame.drop(colName)
58 | })
59 | return dataFrame
60 | }
61 |
62 | function mapColumnsLocal(data, columnName, mappedColumnName, dbResponse) {
63 | const localMapper = createHashMap(dbResponse)
64 | return data.map((row) => row.set(mappedColumnName, localMapper.get(row.get(columnName))))
65 | }
66 |
67 | module.exports = {
68 | renameColumns,
69 | filterReqColumns,
70 | mapNamesToID,
71 | produceMappers,
72 | createHashMap,
73 | produceMapping,
74 | dropColumns,
75 | mapColumnsLocal,
76 | }
77 |
--------------------------------------------------------------------------------
/celery/aan_extensions/DispatcherAgent/tasks.py:
--------------------------------------------------------------------------------
1 | # import ExtractionAgent.tasks
2 | # import TranscriptionAgent.tasks
3 | from celery import chain, group
4 | from celery_worker import app
5 | from BaseAgent import BaseTask
6 | from aan_extensions import TranscriptionAgent, ExtractionAgent, CacheAgent, SummaryAgent, NextBestActionAgent, SentimentAgent
7 |
8 | import logging
9 |
10 | from opentelemetry import trace
11 | from opentelemetry.trace import SpanKind
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | class colors:
16 | OKGREEN = '\033[92m'
17 | OKBLUE = '\033[94m'
18 | ENDC = '\033[0m'
19 |
20 | @app.task(base=BaseTask.BaseTask, bind=True)
21 | def process_transcript(self,topic, message):
22 | result = {}
23 | with trace.get_tracer(__name__).start_as_current_span("process_transcript", kind=SpanKind.PRODUCER) as span:
24 | try:
25 | with trace.get_tracer(__name__).start_as_current_span("dispatch_tasks") as child_span:
26 | TranscriptionAgent.tasks.process_transcript.s(topic, message).apply_async()
27 |
28 | # Moving extraction to after CacheAgent since it needs full transcripts
29 | # ExtractionAgent.tasks.process_transcript.s(topic,message).apply_async()
30 | NextBestActionAgent.tasks.process_transcript.s(topic,message).apply_async()
31 | chained_tasks = chain(
32 | CacheAgent.tasks.process_transcript.s(topic,message),
33 | # NextBestActionAgent.tasks.process_transcript.si(topic,message),
34 | # SummaryAgent.tasks.process_transcript.si(topic,message)
35 | group(
36 | SummaryAgent.tasks.process_transcript.si(topic,message),
37 | ExtractionAgent.tasks.process_transcript.si(topic,message),
38 | SentimentAgent.tasks.process_transcript.si(topic,message)
39 | )
40 | )
41 | chained_tasks.apply_async()
42 | #self.await_sio_emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery')
43 | # self.sio.emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery')
44 | # self.redis_client.append_to_list_json()
45 | except Exception as e:
46 | print(e)
47 | # the return result is stored in the celery backend
48 | return result
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Dashboard/SessionBar/SessionBar.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { AppContext } from "../../../context/context";
3 | import SessionUser from "../SessionUser/SessionUser";
4 | import { Accordion } from "@chakra-ui/react";
5 |
6 | const SessionBar = () => {
7 | const { sessionUsers } = useContext(AppContext);
8 | const [activeSession, setActiveSession] = useState([]);
9 | const [completedSession, setCompletedSession] = useState([]);
10 |
11 | useEffect(() => {
12 | setActiveSession(sessionUsers.filter((session) => session.is_active));
13 | setCompletedSession(sessionUsers.filter((session) => !session.is_active));
14 | }, [sessionUsers]);
15 |
16 | return (
17 |
18 |
19 |
Agent Insights
20 |
Powered by watsonx
21 |
22 |
23 |
24 |
25 |
26 |
27 | Active sessions
28 |
29 |
30 |
31 |
32 | {activeSession.map((session) => (
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Ended sessions
44 |
45 |
46 |
47 |
48 | {completedSession.map((session) => (
49 |
50 | ))}
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default SessionBar;
59 |
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Dashboard/SessionUser/SessionUser.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | AccordionItem,
3 | AccordionButton,
4 | AccordionPanel,
5 | AccordionIcon,
6 | Box,
7 | } from "@chakra-ui/react";
8 | import PropTypes from "prop-types";
9 | import { useContext } from "react";
10 | import { AppContext } from "../../../context/context";
11 | import { sentimentIcons } from "../../../utils/data";
12 | import React from 'react';
13 | import { differenceInMinutes, parseISO, format } from 'date-fns';
14 |
15 | const calculateCallDuration = (startTime, endTime) => {
16 | const start = parseISO(startTime);
17 | const end = parseISO(endTime);
18 | return differenceInMinutes(end, start);
19 | };
20 |
21 | const formatDate = (dateString) => {
22 | const date = parseISO(dateString);
23 | return format(date, 'PPP'); // e.g., Mar 13, 2024
24 | };
25 |
26 | const SessionUser = ({ session }) => {
27 | const { dispatch } = useContext(AppContext);
28 |
29 | const handleClick = () => {
30 | dispatch({ type: "AddCurrentSessionUser", payload: session });
31 | };
32 | // console.log("SESSION DETAILS:");
33 | // console.log(session);
34 | const callDuration = session.time_ended ? calculateCallDuration(session.time_started, session.time_ended) : null;
35 | const callDate = session.time_ended ? formatDate(session.time_started) : null;
36 |
37 | return (
38 |
39 |
40 |
41 |
42 | {session?.phone} {session.is_active ? '' : ` - ${callDate} (${callDuration} mins)`}
43 |
44 |
45 |
46 |
47 |
48 |
49 | Caller ID : {session.caller_id}
50 |
51 |
52 | DID : {session.DID}
53 |
54 |
55 |
Sentiment
{" "}
56 |
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default SessionUser;
70 | SessionUser.propTypes = {
71 | session: PropTypes.any,
72 | };
73 |
--------------------------------------------------------------------------------
/wrapper-ui/src/libs/Mqtt/MqttMethods.js:
--------------------------------------------------------------------------------
1 | // src/libs/MqttMethods.js
2 |
3 | export const transformSessionData = (payload) => {
4 | return {
5 | phone: payload.customer_ani,
6 | caller_id: payload.customer_name || process.env.VITE_MQTT_CALLER_ID,
7 | DID: payload.dnis,
8 | is_active: true,
9 | session_id: payload.session_id,
10 | sentiment: "",
11 | };
12 | };
13 |
14 | export const extractSessionId = (topic) => {
15 | const parts = topic.split("/");
16 | return parts.length >= 2 ? parts[1] : null;
17 | };
18 |
19 | export const transformTranscriptionData = (payload, topic) => {
20 | const sessionId = extractSessionId(topic);
21 | return {
22 | session_id: sessionId,
23 | text: payload.parameters.text,
24 | user_type: payload.parameters.source === "external" ? "customer" : "agent",
25 | seq: payload.parameters.seq, // Add the sequence number to the transformed data
26 | };
27 | };
28 |
29 | export const transformSummaryData = (payload, topic) => {
30 | const sessionId = extractSessionId(topic);
31 | return {
32 | session_id: sessionId,
33 | summary: payload.parameters.text,
34 | };
35 | };
36 |
37 | // Add to MqttMethods.js
38 |
39 | export const transformNextBestActionData = (parameters, sessionId) => {
40 | const actionWithOptions = {
41 | content: parameters.text,
42 | action_id: parameters.action_id,
43 | time: new Date().toISOString(),
44 | is_completed: false,
45 | options: parameters.options || [],
46 | session_id: sessionId,
47 | };
48 |
49 | return {
50 | session_id: sessionId,
51 | action: actionWithOptions,
52 | };
53 | };
54 |
55 | export const markActionAsCompleted = (actionId, sessionId) => {
56 | return {
57 | session_id: sessionId,
58 | action_id: actionId,
59 | };
60 | };
61 |
62 | export const transformSentimentData = (payload, topic) => {
63 | const sessionId = extractSessionId(topic);
64 | const parameters = payload.parameters;
65 | return {
66 | session_id: sessionId,
67 | source: parameters.source,
68 | sadness: parseFloat(parameters.sadness) || 0,
69 | joy: parseFloat(parameters.joy) || 0,
70 | fear: parseFloat(parameters.fear) || 0,
71 | disgust: parseFloat(parameters.disgust) || 0,
72 | anger: parseFloat(parameters.anger) || 0,
73 | };
74 | };
75 |
76 |
77 | export const startSentimentData = (payload, source) => {
78 | return {
79 | session_id: payload.session_id,
80 | source: source,
81 | sadness: 0.5,
82 | joy: 0.5,
83 | fear: 0.5,
84 | disgust: 0.5,
85 | anger: 0.5,
86 | };
87 | };
88 |
89 |
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/MQTTEventPublisher.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const mqtt = require('mqtt');
4 | let mqttClient = null;
5 |
6 | const LOG_LEVEL = process.env.LOG_LEVEL;
7 | const logger = require('pino')({ level: LOG_LEVEL, name: 'MQTTEventPublisher' });
8 |
9 | const EventPublisher = require('./EventPublisher');
10 |
11 | class MQTTEventPublisher extends EventPublisher {
12 |
13 | constructor() {
14 | super();
15 |
16 | let disable_ssl = (process.env.MQTT_BROKER_DISABLE_SLL === 'true');
17 |
18 | let broker_url;
19 | if (!process.env.MQTT_BROKER_URL.includes("//")){
20 | if (disable_ssl == false)
21 | broker_url = "wss://" + process.env.MQTT_BROKER_URL + ":" + process.env.MQTT_BROKER_PORT + process.env.MQTT_BROKER_PATH;
22 | else
23 | broker_url = "ws://" + process.env.MQTT_BROKER_URL + ":" + process.env.MQTT_BROKER_PORT + process.env.MQTT_BROKER_PATH;
24 | }
25 | else{
26 | broker_url = process.env.MQTT_BROKER_URL + ":" + process.env.MQTT_BROKER_PORT + process.env.MQTT_BROKER_PATH;
27 | }
28 |
29 | let username = process.env.MQTT_BROKER_USER_NAME;
30 | let password = process.env.MQTT_BROKER_PASSWORD;
31 | let client_id = "mqtt_client_" + Math.floor((Math.random() * 1000) + 1);
32 |
33 | logger.trace('MQTTEventPublisher: broker_url: ' + broker_url);
34 | logger.trace('MQTTEventPublisher: username: ' + username);
35 | logger.trace('MQTTEventPublisher: password: ' + password);
36 | logger.trace('MQTTEventPublisher: client_id: ' + client_id);
37 |
38 | const options = {
39 | // Clean session
40 | 'clean': true,
41 | 'connectTimeout': 4000,
42 | // Authentication
43 | 'clientId': client_id,
44 | 'username': username,
45 | 'password': password,
46 | 'keepalive': 30
47 | }
48 |
49 | mqttClient = mqtt.connect(broker_url, options);
50 |
51 | mqttClient.on("connect", () => {
52 | logger.debug('MQTTEventPublisher: connected to broker and ready to publish');
53 | });
54 |
55 | mqttClient.on("error", (error) => {
56 | logger.debug('MQTTEventPublisher: failed to connect to broker. Error: ' + error);
57 | });
58 |
59 | return this;
60 | }
61 |
62 | /* eslint-disable class-methods-use-this */
63 | publish(topic, message) {
64 | logger.debug('MQTTEventPublisher: publishing message: ' + message + ' on topic: ' + topic);
65 | mqttClient.publish(topic, message);
66 | }
67 |
68 | destroy() {
69 | // Force the shutdown of the client connection.
70 | mqttClient.end(true);
71 | }
72 | }
73 | module.exports = MQTTEventPublisher;
74 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/server/auth.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | import {Express} from "express";
4 | import passport from "passport";
5 | import session from "express-session";
6 | import crypto from "crypto";
7 |
8 | const WebAppStrategy = require('ibmcloud-appid').WebAppStrategy;
9 |
10 | const AUTH_STRATEGY = process.env.AUTH_STRATEGY ? process.env.AUTH_STRATEGY : "WebAppStrategy";
11 | const AUTH_CALLBACK_URL = process.env.AUTH_CALLBACK_URL ? process.env.AUTH_CALLBACK_URL : "/ibm/cloud/appid/callback";
12 | const AUTH_SESSION_SECRET = process.env.AUTH_SESSION_SECRET ? process.env.AUTH_SESSION_SECRET : crypto.randomBytes(20).toString('hex');
13 | const AUTH_CLIENT_ID = process.env.AUTH_CALLBACK_CLIENT_ID;
14 | const AUTH_OAUTH_SERVER_URL = process.env.AUTH_OAUTH_SERVER_URL;
15 | const AUTH_PROFILES_URL = process.env.AUTH_PROFILES_URL ? process.env.AUTH_PROFILES_URL : "https://us-south.appid.cloud.ibm.com";
16 | const AUTH_APP_ID_SECRET = process.env.AUTH_APP_ID_SECRET;
17 | const AUTH_TENANT_ID = process.env.AUTH_TENANT_ID;
18 |
19 | export default function setupEnvironment(app: Express) {
20 | app.use(
21 | session({
22 | secret: AUTH_SESSION_SECRET,
23 | resave: true,
24 | saveUninitialized: true,
25 | proxy: true,
26 | cookie: {secure: true}
27 | }),
28 | );
29 |
30 | app.use(passport.initialize());
31 | app.use(passport.session());
32 |
33 | let appStrategy;
34 | let appStrategyName;
35 |
36 | switch (AUTH_STRATEGY) {
37 | case "WebAppStrategy":
38 | default:
39 | appStrategy = new WebAppStrategy({
40 | clientId: AUTH_CLIENT_ID,
41 | oauthServerUrl: AUTH_OAUTH_SERVER_URL,
42 | profilesUrl: AUTH_PROFILES_URL,
43 | secret: AUTH_APP_ID_SECRET,
44 | tenantId: AUTH_TENANT_ID,
45 | redirectUri: `http://localhost:3000${AUTH_CALLBACK_URL}`
46 | });
47 |
48 | appStrategyName = "webAppStrategy";
49 | }
50 |
51 |
52 | passport.use(appStrategyName, appStrategy);
53 |
54 | passport.serializeUser((user: any, cb) => cb(null, user));
55 | passport.deserializeUser((obj: any, cb) => cb(null, obj));
56 |
57 | app.get(
58 | AUTH_CALLBACK_URL,
59 | passport.authenticate(appStrategyName, {
60 | failureRedirect: "/error"
61 | }),
62 | );
63 |
64 | app.use(
65 | "/protected",
66 | passport.authenticate(appStrategyName)
67 | );
68 |
69 | app.get("/logout", (req, res, next) => {
70 | req.logout(function (err) {
71 | if (err) {
72 | return next(err);
73 | }
74 | res.clearCookie("refreshToken");
75 | res.redirect("/protected");
76 | });
77 | });
78 |
79 | app.get("/error", (req, res) => {
80 | res.send("Authentication Error");
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/utilities/mono-to-stereo-wav-converter/wav_header_dump.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs');
3 |
4 | // Extract command-line arguments excluding the first two elements (which are 'node' and the script filename)
5 | const arguments = process.argv.slice(2);
6 | const input_filename = arguments[0];
7 |
8 | function startFileAnalysis (){
9 | let input_buffer = fs.readFileSync(input_filename);
10 |
11 | input_wav_headers = parseWavHeaders (input_buffer);
12 |
13 | console.log(JSON.stringify(input_wav_headers, null, 2));
14 |
15 | console.log("Data length: " + input_wav_headers.fileSize);
16 |
17 | }
18 |
19 | // Function to parse the WAV file buffer and extract headers
20 | function parseWavHeaders(buffer) {
21 | const headers = {};
22 | // Check for RIFF chunk header
23 | if (buffer.toString('utf8', 0, 4) !== 'RIFF') {
24 | throw new Error('Invalid WAV file format');
25 | }
26 | // Read the total file size
27 | headers.fileSize = buffer.readUInt32LE(4);
28 |
29 | // Check for WAVE format
30 | if (buffer.toString('utf8', 8, 12) !== 'WAVE') {
31 | throw new Error('Invalid WAV file format');
32 | }
33 | // Check for fmt chunk header
34 | if (buffer.toString('utf8', 12, 16) !== 'fmt ') {
35 | throw new Error('Invalid WAV file format');
36 | }
37 | // Read the format chunk size
38 | headers.fmtChunkSize = buffer.readUInt32LE(16);
39 | // Read the audio format (PCM should be 1)
40 | headers.audioFormat = buffer.readUInt16LE(20);
41 | // Read the number of channels
42 | headers.numChannels = buffer.readUInt16LE(22);
43 |
44 | // Read the sample rate
45 | headers.sampleRate = buffer.readUInt32LE(24);
46 | // Read the byte rate
47 | headers.byteRate = buffer.readUInt32LE(28);
48 | // Read the block align
49 | headers.blockAlign = buffer.readUInt16LE(32);
50 | // Read the bits per sample
51 | headers.bitsPerSample = buffer.readUInt16LE(34);
52 |
53 | // Now find the offest to the data chunk
54 | let offset = 20 + headers.fmtChunkSize;
55 | while (offset < headers.fileSize)
56 | {
57 | if (buffer.toString('utf8', offset, offset + 4) == 'data') {
58 | // Read the data chunk size
59 | headers.dataSize = buffer.readUInt32LE(offset + 4);
60 | headers.dataOffset = offset + 8;
61 | break
62 | }
63 | else {
64 | console.log('Non-data subchunk found: type = ' + buffer.toString('utf8', offset, offset + 4) + " size = " + buffer.readUInt32LE(offset + 4));
65 | offset += 8 + buffer.readUInt32LE(offset + 4);
66 | }
67 | }
68 |
69 | return headers;
70 | }
71 |
72 | startFileAnalysis();
73 |
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Modal/Setting/SettingModal.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalOverlay,
4 | ModalContent,
5 | ModalHeader,
6 | ModalFooter,
7 | ModalBody,
8 | ModalCloseButton,
9 | Button,
10 | useDisclosure,
11 | Input,
12 | } from "@chakra-ui/react";
13 | import PropTypes from "prop-types";
14 |
15 | import { IoSettingsOutline } from "react-icons/io5";
16 |
17 | export function SettingModal({ children }) {
18 | const { isOpen, onOpen, onClose } = useDisclosure();
19 | return (
20 | <>
21 |
{children}
22 |
23 |
24 |
25 |
26 |
27 |
28 | Settings
29 |
30 |
31 |
32 |
33 |
34 | Username{" "}
35 |
36 |
40 |
41 |
42 |
43 | Password{" "}
44 |
45 |
46 |
47 |
48 |
49 | Host *{" "}
50 |
51 |
52 |
53 |
54 |
55 | Port *{" "}
56 |
57 |
58 |
59 |
60 |
61 | Path *{" "}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Close
70 |
71 | onClose()}>
72 | Save
73 |
74 |
75 |
76 |
77 | >
78 | );
79 | }
80 |
81 | SettingModal.propTypes = {
82 | children: PropTypes.any,
83 | };
84 |
--------------------------------------------------------------------------------
/wrapper-ui/src/components/Dashboard/SentimentProgress/SentimentProgress.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react';
2 | import { RadarChart } from '@carbon/charts-react';
3 | import '@carbon/charts/styles.css';
4 | import { AppContext } from "../../../context/context";
5 |
6 | const SentimentProgress = () => {
7 | const { currentSessionUser, sentimentData } = useContext(AppContext);
8 | const [chartData, setChartData] = useState([]);
9 |
10 | useEffect(() => {
11 | if (!currentSessionUser || !sentimentData.length) return;
12 |
13 | const sessionSentiment = sentimentData.filter(data => data.session_id === currentSessionUser?.session_id);
14 |
15 | if (!sessionSentiment.length) return;
16 |
17 | const externalSentiment = sessionSentiment.filter(data => data.source === "external").pop() || {};
18 | const internalSentiment = sessionSentiment.filter(data => data.source === "internal").pop() || {};
19 |
20 | const newChartData = [
21 | { group: "Customer", key: "Sadness", value: externalSentiment.sadness * 100 },
22 | { group: "Customer", key: "Joy", value: externalSentiment.joy * 100 },
23 | { group: "Customer", key: "Fear", value: externalSentiment.fear * 100 },
24 | { group: "Customer", key: "Disgust", value: externalSentiment.disgust * 100 },
25 | { group: "Customer", key: "Anger", value: externalSentiment.anger * 100 },
26 | { group: "Agent", key: "Sadness", value: internalSentiment.sadness * 100 },
27 | { group: "Agent", key: "Joy", value: internalSentiment.joy * 100 },
28 | { group: "Agent", key: "Fear", value: internalSentiment.fear * 100 },
29 | { group: "Agent", key: "Disgust", value: internalSentiment.disgust * 100 },
30 | { group: "Agent", key: "Anger", value: internalSentiment.anger * 100 }
31 | ];
32 |
33 | setChartData(newChartData);
34 | }, [currentSessionUser, sentimentData]);
35 |
36 | const chartOptions = {
37 | title: "Sentiment Analysis",
38 | radar: {
39 | axes: {
40 | angle: "key",
41 | value: "value"
42 | },
43 | alignment: "center"
44 | },
45 | data: {
46 | groupMapsTo: "group"
47 | },
48 | legend: {
49 | alignment: "center"
50 | },
51 | height: "400px"
52 | };
53 |
54 | return (
55 |
56 | {chartData.length > 0 ? (
57 |
58 | ) : (
59 |
Loading sentiment data...
60 | )}
61 |
62 | );
63 | };
64 |
65 | export default SentimentProgress;
66 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing In General
2 | Our project welcomes external contributions. If you have an itch, please feel
3 | free to scratch it.
4 |
5 | To contribute code or documentation, please submit a **FIXME** [pull request](https://github.com/ibm/agent-assist/pulls).
6 |
7 | A good way to familiarize yourself with the codebase and contribution process is
8 | to look for and tackle low-hanging fruit in the **FIXME** [issue tracker](https://github.com/ibm/agent-assist/issues).
9 | Before embarking on a more ambitious contribution, please quickly [get in touch](#communication) with us.
10 |
11 | **Note: We appreciate your effort, and want to avoid a situation where a contribution
12 | requires extensive rework (by you or by us), sits in backlog for a long time, or
13 | cannot be accepted at all!**
14 |
15 | ### Proposing new features
16 |
17 | If you would like to implement a new feature, please **FIXME** [raise an issue](https://github.com/ibm/agent-assist/issues)
18 | before sending a pull request so the feature can be discussed. This is to avoid
19 | you wasting your valuable time working on a feature that the project developers
20 | are not interested in accepting into the code base.
21 |
22 | ### Fixing bugs
23 |
24 | If you would like to fix a bug, please **FIXME** [raise an issue](https://github.com/ibm/agent-assist/issues) before sending a
25 | pull request so it can be tracked.
26 |
27 | ### Merge approval
28 |
29 | The project maintainers use LGTM (Looks Good To Me) in comments on the code
30 | review to indicate acceptance. A change requires LGTMs from two of the
31 | maintainers of each component affected.
32 |
33 | For a list of the maintainers, see the [MAINTAINERS.md](MAINTAINERS.md) page.
34 |
35 | ## Legal
36 |
37 | Each source file must include a license header for the Apache
38 | Software License 2.0. Using the SPDX format is the simplest approach.
39 | e.g.
40 |
41 | ```
42 | /*
43 | Copyright
All Rights Reserved.
44 |
45 | SPDX-License-Identifier: Apache-2.0
46 | */
47 | ```
48 |
49 | We have tried to make it as easy as possible to make contributions. This
50 | applies to how we handle the legal aspects of contribution. We use the
51 | same approach - the [Developer's Certificate of Origin 1.1 (DCO)](https://github.com/hyperledger/fabric/blob/master/docs/source/DCO1.1.txt) - that the Linux® Kernel [community](https://elinux.org/Developer_Certificate_Of_Origin)
52 | uses to manage code contributions.
53 |
54 | We simply ask that when submitting a patch for review, the developer
55 | must include a sign-off statement in the commit message.
56 |
57 | Here is an example Signed-off-by line, which indicates that the
58 | submitter accepts the DCO:
59 |
60 | ```
61 | Signed-off-by: John Doe
62 | ```
63 |
64 | You can include this automatically when you commit a change to your
65 | local git repository using the following command:
66 |
67 | ```
68 | git commit -s
69 | ```
70 |
71 |
--------------------------------------------------------------------------------
/celery/BaseAgent/BaseTask test.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 | import socketio
3 | import redis
4 | import os
5 | import json
6 |
7 | from celery import Task
8 |
9 |
10 | class BaseTask(Task):
11 | _sio = None
12 | _redis_client = None
13 | _sio_status = False
14 |
15 | async def initialize_sio(self):
16 | if self._sio is None:
17 | self._sio = socketio.AsyncClient(logger=True, engineio_logger=True)
18 | await self._sio.connect(
19 | os.getenv("ANN_SOCKETIO_SERVER", "http://localhost:8000"),
20 | namespaces=["/celery"],
21 | )
22 | print("Socketio client initialized")
23 | self._sio_status = True
24 | else:
25 | print("Using existing socketio client")
26 |
27 | async def sio(self):
28 | if self._sio is None:
29 | await self.initialize_sio()
30 | print(self._sio)
31 | return self._sio
32 |
33 | async def await_sio_emit(self, event, data, namespace):
34 | sio = await self.sio()
35 | return await sio.emit(event, data, namespace)
36 |
37 | async def async_sio_emit(self, event, data, namespace):
38 | sio = await self.sio()
39 | print(sio)
40 | sio.emit(event, data, namespace)
41 |
42 | def get_sio_status(self):
43 | return self._sio_status
44 |
45 | @property
46 | def redis_client(self):
47 | if self._redis_client is None:
48 | self._redis_client = redis.StrictRedis(
49 | host=os.getenv("AAN_REDIS_HOST", "localhost"),
50 | port=os.getenv("AAN_REDIS_PORT", 6379),
51 | db=os.getenv("AAN_REDIS_DB_INDEX", 2),
52 | )
53 | print("Starting Redis client")
54 | return self._redis_client
55 |
56 | def create_json(self, key, value):
57 | json_value = json.dumps(value)
58 | self._redis_client.set(key, json_value)
59 |
60 | def read_json(self, key):
61 | json_value = self._redis_client.get(key)
62 | if json_value:
63 | return json.loads(json_value)
64 | return None
65 |
66 | def update_json(self, key, value):
67 | json_value = json.dumps(value)
68 | if self._redis_client.exists(key):
69 | self._redis_client.set(key, json_value)
70 | else:
71 | raise KeyError(f"Key '{key}' does not exist in Redis")
72 |
73 | def delete(self, key):
74 | if self._redis_client.exists(key):
75 | self._redis_client.delete(key)
76 | else:
77 | raise KeyError(f"Key '{key}' does not exist in Redis")
78 |
79 | def append_to_list_json(self, key, value):
80 | """
81 | Append a value to the list stored at the given key.
82 | If the key does not exist, a new list is created.
83 | """
84 | json_value = json.dumps(value)
85 | self._redis_client.rpush(key, json_value)
86 |
--------------------------------------------------------------------------------
/utilities/mono-to-stereo-wav-converter/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
106 |
107 | # dependencies
108 | /node_modules
109 | /.pnp
110 | .pnp.js
111 | .yarn/install-state.gz
112 |
113 | # testing
114 | /coverage
115 |
116 | # next.js
117 | /.next/
118 | /out/
119 |
120 | # production
121 | /build
122 |
123 | # misc
124 | .DS_Store
125 | *.pem
126 |
127 | # debug
128 | npm-debug.log*
129 | yarn-debug.log*
130 | yarn-error.log*
131 |
132 | # local env files
133 | .env*.local
134 |
135 | # vercel
136 | .vercel
137 |
138 | # typescript
139 | *.tsbuildinfo
140 | next-env.d.ts
141 |
142 | # certs used for testing
143 | ca.crt
144 |
145 | # mac
146 | .DS_Store
147 |
148 | # Keep the test wav files out of git
149 | *.wav
150 |
151 |
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/WatsonSpeechToTextEngine.js:
--------------------------------------------------------------------------------
1 |
2 | const SpeechToTextV1 = require('ibm-watson/speech-to-text/v1');
3 | const SpeechToTextEngine = require('./SpeechToTextEngine');
4 | const { BasicAuthenticator, IamAuthenticator } = require('ibm-watson/auth');
5 |
6 | const LOG_LEVEL = process.env.LOG_LEVEL;
7 | const logger = require('pino')({ level: LOG_LEVEL, name: 'WatsonSpeechToTextEngine' });
8 |
9 | const WatsonSpeechToTextCredentials = {
10 | 'username': process.env.WATSON_STT_USERNAME,
11 | 'password': process.env.WATSON_STT_PASSWORD
12 | };
13 |
14 | const { username, password } = WatsonSpeechToTextCredentials;
15 | const basicAuthenticator = new BasicAuthenticator({ username, password });
16 |
17 | const speechToText = new SpeechToTextV1({
18 | authenticator: basicAuthenticator,
19 | url: process.env.WATSON_STT_URL
20 | });
21 |
22 | class WatsonSpeechToTextEngine extends SpeechToTextEngine {
23 | /**
24 | * Creates an instace of the WatsonSpeechToTextEngine
25 | */
26 | constructor() {
27 | super();
28 |
29 | // Pass these parameters into the STT object.
30 | const params = {
31 | 'contentType': 'audio/basic', // Encoding of the audio, defaults to mulaw (pcmu) at 8kHz
32 | 'action': 'start', // Start message for Watson Speech To Text
33 | 'interimResults': true,
34 | //'lowLatency': true,
35 | 'inactivityTimeout': -1,
36 | 'model': process.env.WATSON_STT_MODEL, // Use Narrowband Model for english at 8kHZ
37 | 'objectMode': true,
38 | 'endOfPhraseSilenceTime': parseFloat(process.env.WATSON_STT_END_OF_PHRASE_SILENCE_TIME),
39 | 'smartFormatting': true,
40 | 'splitTranscriptAtPhraseEnd': true,
41 | 'backgroundAudioSuppression': 0.5,
42 | 'speechDetectorSensitivity': 0.4,
43 | 'timestamps': true
44 | };
45 |
46 | logger.debug (params, 'Watson STT engine parameters');
47 |
48 | // Watson Node-SDK supports NodeJS streams, its open source you can
49 | // see the implementation of the recognize stream here: https://github.com/watson-developer-cloud/node-sdk/blob/master/lib/recognize-stream.ts
50 | // As a result, we can return recognize stream as our stream for the adapter
51 | // The idea is your implementation must emit 'data' events that are formatted as Watson results
52 | // See the WatsonSpeechToText API https://www.ibm.com/watson/developercloud/speech-to-text/api/v1/#recognize_sessionless_nonmp12
53 | this.recognizeStream = speechToText.recognizeUsingWebSocket(params);
54 |
55 | this.recognizeStream.destroy = () => {
56 | this.recognizeStream.stop();
57 | };
58 |
59 | return this.recognizeStream;
60 | }
61 | /* eslint-disable class-methods-use-this */
62 | _read() {}
63 |
64 | _write() {}
65 | }
66 | module.exports = WatsonSpeechToTextEngine;
67 |
--------------------------------------------------------------------------------
/celery/old-tasks.py:
--------------------------------------------------------------------------------
1 |
2 | import time
3 | from celery import Celery
4 | import socketio
5 |
6 | from celery import Task
7 |
8 | class SocketioTask(Task):
9 | _sio = None
10 |
11 | @property
12 | def sio(self):
13 | if self._sio is None:
14 | self._sio = socketio.Client(logger=True, engineio_logger=True)
15 | self._sio.connect('http://localhost:8000', namespaces=['/celery'])
16 | print("Starting Socketio")
17 | return self._sio
18 |
19 |
20 | app = Celery('tasks', broker='amqp://admin:adminpass@localhost:5672', backend="redis://localhost:6379/1")
21 | #- CELERY_BROKER_LINK=${CELERY_BROKER_LINK-amqp://admin:adminpass@rabbitmq}
22 |
23 | class colors:
24 | OKGREEN = '\033[92m'
25 | OKBLUE = '\033[94m'
26 | ENDC = '\033[0m'
27 |
28 |
29 | # @sio.event
30 | # def connect():
31 | # print('Connected to API Socketio')
32 |
33 | # @sio.event
34 | # def disconnect():
35 | # print('Disconnected from API Socketio')
36 |
37 | # # Start Socket.IO client
38 | # def start_socket_client():
39 | # sio.connect('http://localhost:8000') # Adjust URL as per your setup
40 | # # sio.connect('http://localhost:8000', namespaces=['/celery']) # Adjust URL as per your setup
41 | # #sio.wait()
42 |
43 |
44 | # print("Starting Socketio")
45 | # start_socket_client()
46 |
47 | # Define the task
48 | @app.task
49 | def add(x, y):
50 | result = x + y
51 | adjusted_sleep_time = result * 2 / 1000 # Convert total to seconds and double it
52 | # Simulate a blocking wait
53 | time.sleep(adjusted_sleep_time)
54 |
55 | print(f"The result of {x} + {y} is {colors.OKGREEN}{result}{colors.ENDC} at {colors.OKBLUE}{adjusted_sleep_time:.3f} seconds{colors.ENDC}")
56 |
57 | # the return result is stored in the celery backend
58 | return result
59 |
60 | # Define the task
61 | @app.task(base=SocketioTask, bind=True)
62 | def echo(self,topic, message):
63 | result = topic + '---' + message
64 | # adjusted_sleep_time = result * 2 / 1000 # Convert total to seconds and double it
65 | # # Simulate a blocking wait
66 | # time.sleep(adjusted_sleep_time)
67 |
68 | print(f"Received {colors.OKGREEN}{topic}{colors.ENDC} + {colors.OKBLUE}{message}{colors.ENDC}")
69 | # emit(event, data=None, room=None, skip_sid=None, namespace=None)
70 | print(self.sio)
71 | try:
72 | self.sio.emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery') #
73 | except Exception as e:
74 | print(e)
75 | # the return result is stored in the celery backend
76 | return result
77 |
78 |
79 | # @sio.on('connect')
80 | # def on_connect():
81 | # print("I'm connected to the default namespace!")
82 | # sio.emit('celeryMessage', {'payloadString': 'test', 'destinationName': 'atopic'})
83 |
84 |
85 |
86 | # Start the Celery worker
87 | if __name__ == '__main__':
88 | app.worker_main()
89 | # print("Starting Socketio")
90 | # start_socket_client()
--------------------------------------------------------------------------------
/watson-stt-stream-connector/lib/StreamConnectorServer.js:
--------------------------------------------------------------------------------
1 |
2 | // const setupTelemetry = require('./setupTelemetry');
3 | // const provider = setupTelemetry();
4 |
5 | const WebSocket = require('ws');
6 | const WebSocketServer = require('ws').Server;
7 |
8 | const EventPublisher = require('./CeleryEventPublisher');
9 | let eventPublisher = null;
10 |
11 | // CCaaS specific adapters currently supported
12 | const GenesysAudioHookAdapter = require('./GenesysAudioHookAdapter');
13 | const MonoChannelStreamingAdapter = require('./MonoChannelStreamingAdapter');
14 | const SiprecStreamingAdapter = require('./SiprecStreamingAdapter');
15 |
16 | const LOG_LEVEL = process.env.LOG_LEVEL;
17 | const logger = require('pino')({ level: LOG_LEVEL, name: 'StreamConnectorServer' });
18 |
19 | /**
20 | *
21 | * @returns
22 | */
23 | let wsServer = null;
24 | function startServer() {
25 | return new Promise((resolve, reject) => {
26 | // Setup event publisher
27 | eventPublisher = new EventPublisher();
28 |
29 | try {
30 | wsServer = new WebSocketServer({ port: process.env.DEFAULT_SERVER_LISTEN_PORT });
31 | } catch (e) {
32 | return reject(e);
33 | }
34 |
35 | wsServer.on('error', (error) => {
36 | logger.error(error);
37 | });
38 |
39 | wsServer.on('listening', () => {
40 | logger.info(`Speech To Text Adapter has started. Listening on port = ${process.env.DEFAULT_SERVER_LISTEN_PORT}`);
41 | resolve();
42 | });
43 |
44 | // As new adapters are added this is where they will be triggered
45 | if (process.env.STREAM_ADAPTER_TYPE == 'GenesysAudioHookAdapter'){
46 | GenesysAudioHookAdapter.setEventPublisher(eventPublisher);
47 | wsServer.on('connection', GenesysAudioHookAdapter.handleAudioHookConnection);
48 | }
49 | else if (process.env.STREAM_ADAPTER_TYPE == 'MonoChannelStreamingAdapter'){
50 | MonoChannelStreamingAdapter.setEventPublisher(eventPublisher);
51 | wsServer.on('connection', MonoChannelStreamingAdapter.handleMonoChannelStreamingConnection);
52 | }
53 | else if (process.env.STREAM_ADAPTER_TYPE == 'SiprecStreamingAdapter'){
54 | SiprecStreamingAdapter.setEventPublisher(eventPublisher);
55 | wsServer.on('connection', SiprecStreamingAdapter.handleSiprecStreamingConnection);
56 | }
57 | else
58 | logger.error(`Unknown adapter type`);
59 |
60 | return wsServer;
61 | });
62 | }
63 | module.exports.start = startServer;
64 |
65 | /**
66 | *
67 | * @returns
68 | */
69 | function stopServer() {
70 | return new Promise((resolve, reject) => {
71 |
72 | if (eventPublisher != null){
73 | eventPublisher.destroy();
74 | eventPublisher = null;
75 | }
76 |
77 | if (wsServer === null) {
78 | return reject(new Error('server not started'));
79 | }
80 |
81 | wsServer.close((err) => {
82 | if (err) {
83 | return reject(err);
84 | }
85 | return resolve();
86 | });
87 |
88 | return wsServer;
89 | });
90 | }
91 | module.exports.stop = stopServer;
92 |
93 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/client/components/NextBestActions/BestAction.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import * as styles from "./BestAction.module.scss";
4 | import {ClickableTile, Tooltip} from "@carbon/react";
5 | import {useTranslation} from "react-i18next";
6 | import {v4 as uuid} from 'uuid';
7 | import {Action, ActionState} from "./NextBestActions";
8 | import {Checkmark, CloseFilled, Hourglass, Result} from "@carbon/icons-react";
9 | import sanitizeHtml from 'sanitize-html';
10 | import {useEffect, useState} from "react";
11 | import {useSocket} from "@client/providers/Socket";
12 |
13 | type ActionOptions = {
14 | icon: any,
15 | style: any
16 | }
17 |
18 | const BestAction = ({action, updateAction, sendManualCompletion}: { action: Action, updateAction: (action: Action) => void, sendManualCompletion: ()=> void }) => {
19 | const {t} = useTranslation();
20 | const {socket} = useSocket();
21 |
22 | const getIcon = (state: ActionState) => {
23 | switch (state) {
24 | case ActionState.active:
25 | return Result;
26 | case ActionState.stale:
27 | return Hourglass;
28 | case ActionState.expired:
29 | return CloseFilled;
30 | case ActionState.complete:
31 | return Checkmark;
32 | }
33 | };
34 |
35 | const [actionOptions, setActionOptions] = useState({
36 | icon: getIcon(action.state),
37 | style: styles[action.state]
38 | });
39 |
40 | useEffect(() => {
41 | const interval = setInterval(() => {
42 | if (action?.state !== ActionState.complete) {
43 | const passedTime = (new Date().getTime() - action.createdAt) / 1000;
44 |
45 | if (passedTime > 15 && passedTime < 30) {
46 | action.state = ActionState.stale;
47 | } else if (passedTime >= 30) {
48 | action.state = ActionState.expired;
49 | } else {
50 | action.state = ActionState.active;
51 | }
52 | }
53 |
54 | setActionOptions({
55 | icon: getIcon(action.state),
56 | style: styles[action.state]
57 | });
58 |
59 | if (action?.state === ActionState.expired || action?.state === ActionState.complete) {
60 | clearInterval(interval);
61 | }
62 | }, 1000);
63 | return () => clearInterval(interval);
64 | }, [action]);
65 |
66 | const completeAction = () => {
67 | if (action?.state !== ActionState.complete) {
68 | action.state = ActionState.complete;
69 | updateAction(action);
70 | setActionOptions({
71 | icon: getIcon(action.state),
72 | style: styles[action.state]
73 | });
74 | sendManualCompletion()
75 | }
76 | }
77 |
78 | return (
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default BestAction;
--------------------------------------------------------------------------------
/celery/aan_extensions/CacheAgent/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 | from celery_worker import app
3 | from BaseAgent import BaseTask
4 | import logging
5 | import json
6 |
7 | from opentelemetry import trace
8 | from opentelemetry.trace import SpanKind
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | class colors:
13 | OKGREEN = '\033[92m'
14 | OKBLUE = '\033[94m'
15 | ENDC = '\033[0m'
16 |
17 | @app.task(base=BaseTask.BaseTask, bind=True)
18 | def process_transcript(self,topic, message):
19 | with trace.get_tracer(__name__).start_as_current_span("process_transcript", kind=SpanKind.PRODUCER) as span:
20 | result = topic + '---' + message
21 | print(f"CacheAgent {colors.OKGREEN}{topic}{colors.ENDC} + {colors.OKBLUE}{message}{colors.ENDC}")
22 | # print(self.sio)
23 | message_headers = process_transcript.request.headers
24 |
25 | # Extract baggage items from message headers
26 | # Seems like the node app isn't sending any baggage properly from the auto instrumentation
27 | baggage = {}
28 | print(message_headers)
29 | if message_headers is not None and not message_headers:
30 | for key, value in message_headers.items():
31 | logger.debug(f"headers: {key}={value}")
32 | print(f"headers: {key}={value}")
33 | if key.startswith('baggage-'):
34 | baggage[key[len('baggage-'):]] = value
35 |
36 | # Process baggage items as needed
37 | for key, value in baggage.items():
38 | logger.debug(f"Baggage: {key}={value}")
39 |
40 | try:
41 | with trace.get_tracer(__name__).start_as_current_span("save_redis") as child_span:
42 | #self.await_sio_emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery')
43 | #self.sio.emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery')
44 | #{"type":"transcription","parameters":{"source":"internal","text":"excellent okay what color did you want the new yorker in ","seq":7,"timestamp":24.04}} on topic: agent-assist/87ba0766-efc7-42c8-b2ec-af829f6b73ce/transcription
45 | #{"type":"session_ended"} on topic: agent-assist/87ba0766-efc7-42c8-b2ec-af829f6b73ce
46 | client_id = self.extract_client_id(topic)
47 | logger.debug(f"client_id: {client_id}")
48 |
49 | try:
50 | message_data = json.loads(message)
51 | if message_data.get("type", "") == "transcription":
52 | transcript_obj = message_data.get("parameters", {})#.get("text", None)
53 | print(f"Saving rpush {transcript_obj}")
54 | self.redis_client.rpush(client_id, json.dumps(transcript_obj))
55 | except (json.JSONDecodeError, AttributeError):
56 | return None
57 | except Exception as e:
58 | print(e)
59 | # the return result is stored in the celery backend
60 | return result
--------------------------------------------------------------------------------
/wrapper-ui/src/App.jsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { Routes, Route } from "react-router-dom";
3 | import Dashboard from "./pages/Dashboard/Dashboard";
4 | import { useContext, useEffect } from "react";
5 | import useSocketio from "./hooks/useSocketio";
6 |
7 | import { sessionUsers } from "./utils/data";
8 | import { AppContext } from "./context/context";
9 |
10 | function App() {
11 | // const { initializeMqtt } = useMqqt();
12 | const { dispatch } = useContext(AppContext);
13 |
14 |
15 | const {socket, connected, error} = useSocketio();
16 | // useEffect(() => {
17 | // initializeMqtt();
18 | // }, []);
19 |
20 | useEffect(() => {
21 | const fetchAllSessionsFromDb = async () => {
22 | const url = "http://localhost/event-archiver/agent-assist-search/sessions?state=inactive";
23 | const apiKey = "";
24 |
25 | try {
26 | const response = await fetch(url, {
27 | method: 'GET',
28 | headers: {
29 | 'Authorization': `ApiKey ${apiKey}`,
30 | },
31 | });
32 |
33 | if (!response.ok) {
34 | throw new Error(`HTTP error! status: ${response.status}`);
35 | }
36 |
37 | // Get the response as text
38 | let responseText = await response.text();
39 |
40 | // Find the first occurrence of '[' which marks the beginning of your JSON array
41 | const startIndex = responseText.indexOf('[');
42 | if (startIndex === -1) {
43 | throw new Error('Valid JSON not found in response');
44 | }
45 |
46 | // Extract the JSON string from the startIndex to the end
47 | const jsonString = responseText.substring(startIndex);
48 |
49 | // Parse the extracted JSON string
50 | const data = JSON.parse(jsonString);
51 | console.log(data);
52 | dispatch({ type: "AddAllSessions", payload: data });
53 | } catch (error) {
54 | console.error("Error fetching session data:", error);
55 | }
56 | };
57 |
58 | fetchAllSessionsFromDb();
59 | }, [dispatch]);
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | /**
68 | *
69 | * a function that adds a new session to the global state of the app
70 | */
71 | // const addNewSession = () => {
72 | // const new_session = {
73 | // phone: "+31515926535",
74 | // caller_id: "Hary lwinson",
75 | // DID: "+92234S678901",
76 | // is_active: true,
77 | // session_id: "1523367890ABCDEF",
78 | // sentiment: "happy",
79 | // };
80 |
81 | // dispatch({ type: "AddNewSession", payload: new_session });
82 | // };
83 |
84 | return (
85 | <>
86 |
87 |
88 | } />
89 |
90 | {/* Old way
91 | } />
92 | } /> */}
93 |
94 |
95 | >
96 | );
97 | }
98 |
99 | export default App;
100 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "agent-dashboard",
3 | "version": "0.0.1",
4 | "repository": "https://github.com/ibm/agent-assist",
5 | "license": "Apache-2.0",
6 | "scripts": {
7 | "start": "node ./dist/server/index.js",
8 | "watch": "yarn run clean && webpack --color --progress --watch --env WATCH=true --mode=development",
9 | "build": "webpack --color --progress --mode=production",
10 | "test": "cross-env jest --forceExit --coverage --detectOpenHandles",
11 | "clean": "rimraf ./dist"
12 | },
13 | "scripts-info": {
14 | "start": "Run the node server",
15 | "watch": "Clean, run the webpack build, run the node server, and watch for changes",
16 | "build": "Run webpack build",
17 | "test": "Run the jest unit tests",
18 | "clean": "Remove the dist folder"
19 | },
20 | "dependencies": {
21 | "@carbon/colors": "^11.26.0",
22 | "@carbon/icons-react": "^11.49.0",
23 | "@carbon/react": "^1.66.0",
24 | "@carbon/themes": "^11.40.0",
25 | "@carbon/type": "^11.31.0",
26 | "@opentelemetry/api": "^1.9.0",
27 | "@opentelemetry/auto-instrumentations-node": "^0.56.1",
28 | "@opentelemetry/sdk-metrics": "^1.30.1",
29 | "@opentelemetry/sdk-node": "^0.57.2",
30 | "@opentelemetry/sdk-trace-node": "^1.27.0",
31 | "express": "^4.19.2",
32 | "express-session": "^1.18.0",
33 | "fp-ts": "^2.16.5",
34 | "helmet": "^7.1.0",
35 | "http-proxy-middleware": "^3.0.0",
36 | "i18next": "^23.11.3",
37 | "i18next-browser-languagedetector": "^7.2.1",
38 | "i18next-http-backend": "^2.5.1",
39 | "ibmcloud-appid": "^7.0.0",
40 | "io-ts": "^2.2.21",
41 | "lodash": "^4.17.21",
42 | "passport": "^0.7.0",
43 | "react": "^18.3.1",
44 | "react-dom": "^18.3.1",
45 | "react-i18next": "^14.1.1",
46 | "sanitize-html": "^2.13.0",
47 | "socket.io": "^4.7.5",
48 | "socket.io-client": "^4.7.5",
49 | "socket.io-react-hook": "^2.4.4",
50 | "uuid": "^9.0.1"
51 | },
52 | "devDependencies": {
53 | "@axe-core/react": "^4.9.0",
54 | "@types/express": "^4.17.21",
55 | "@types/express-session": "^1.18.0",
56 | "@types/http-proxy": "^1.17.14",
57 | "@types/lodash": "^4.17.1",
58 | "@types/node": "^20.12.8",
59 | "@types/passport": "^1.0.16",
60 | "@types/react": "npm:types-react@beta",
61 | "@types/react-dom": "npm:types-react-dom@beta",
62 | "@types/sanitize-html": "^2.11.0",
63 | "@types/uuid": "^9.0.8",
64 | "copy-webpack-plugin": "^12.0.2",
65 | "css-loader": "^7.1.1",
66 | "fast-sass-loader": "^2.0.1",
67 | "fork-ts-checker-webpack-plugin": "^9.0.2",
68 | "html-webpack-plugin": "^5.6.0",
69 | "jest": "^29.7.0",
70 | "jest-enzyme": "^7.1.2",
71 | "nodemon": "^3.1.0",
72 | "rimraf": "^5.0.5",
73 | "sass": "^1.76.0",
74 | "source-map-loader": "^5.0.0",
75 | "style-loader": "^4.0.0",
76 | "ts-jest": "^29.1.2",
77 | "ts-loader": "^9.5.1",
78 | "tslint": "^6.1.3",
79 | "tslint-loader": "^3.5.4",
80 | "tslint-react": "^5.0.0",
81 | "typescript": "^5.4.5",
82 | "webpack": "^5.91.0",
83 | "webpack-cli": "^5.1.4",
84 | "webpack-node-externals": "^3.0.0",
85 | "webpack-shell-plugin-next": "^2.3.1"
86 | },
87 | "overrides": {
88 | "@types/react": "npm:types-react@beta",
89 | "@types/react-dom": "npm:types-react-dom@beta"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/api-server/index.js:
--------------------------------------------------------------------------------
1 |
2 | // import env
3 | require('dotenv-expand')(require('dotenv').config())
4 |
5 | const express = require('express')
6 | const path = require('path')
7 | const http = require('http')
8 | const compression = require('compression')
9 | const hpp = require('hpp')
10 | const morgan = require('morgan')
11 | const cors = require('cors')
12 | const passport = require('passport')
13 |
14 | const { notFound, errorHandler } = require('./middlewares')
15 | const config = require('./utils/config')
16 | const { configurePassportJwt } = require('./oidc/passportConfig')
17 |
18 | const forceSSL = config.FORCE_SSL === 'true'
19 | const PORT = config.PORT || 8000
20 |
21 | // express app
22 | const app = express()
23 |
24 | // Don't expose any software information to hackers.
25 | app.disable('x-powered-by')
26 |
27 | // Response compression.
28 | app.use(compression({ level: 9 }))
29 |
30 | // Use CORS middleware with options
31 | //app.use(cors(corsOptions));
32 | app.use(cors())
33 |
34 | // Prevent HTTP Parameter pollution.
35 | app.use(hpp())
36 |
37 | // Enable logging
38 | app.use(morgan('dev'))
39 |
40 | if (forceSSL) {
41 | // Enable reverse proxy support in Express. This causes the
42 | // the "X-Forwarded-Proto" header field to be trusted so its
43 | // value can be used to determine the protocol.
44 | app.enable('trust proxy')
45 |
46 | app.use((req, res, next) => {
47 | if (req.secure) {
48 | // request was via https, so do no special handling
49 | next()
50 | } else {
51 | // request was via http, so redirect to https
52 | res.redirect(`https://${req.headers.host}${req.url}`)
53 | }
54 | })
55 | }
56 |
57 | // this is used to set up a JWT verifier to use when OIDC is enabled
58 | const { configuredPassport, authenticateRequests, authenticateRequestsSocketIo } = configurePassportJwt(
59 | passport,
60 | !!config.OIDC_ISSUER,
61 | config.OIDC_ISSUER,
62 | config.OIDC_ISSUER_JWKS_URI || `${config.OIDC_ISSUER}/publickeys`
63 | )
64 |
65 | // turn on OIDC workflow if used
66 | if (config.OIDC_ISSUER) {
67 | app.use(configuredPassport.initialize())
68 | }
69 |
70 |
71 | const staticFilesPath = path.join(__dirname, 'public') // from client/build (copied via dockerfile)
72 | app.use(express.static(staticFilesPath))
73 |
74 | // all other requests, serve index.html
75 | // app.get('/*', (req, res) => {
76 | // res.sendFile(path.join(staticFilesPath, 'index.html'))
77 | // })
78 |
79 | // 404 Handler for api routes
80 | app.use(notFound)
81 |
82 | // Error Handler
83 | app.use(errorHandler)
84 |
85 | let pool, io
86 |
87 | const server = http.createServer(app)
88 |
89 | const { configureSocketIo } = require('./socketio/configureSocketio')
90 | if (config.SOCKETIO_DB_URI) {
91 | // disable pool for now
92 | // pool = require('./socketio/configurePool')
93 | // io = configureSocketIo(server, pool, authenticateRequestsSocketIo)
94 | }
95 | io = configureSocketIo(server, pool, authenticateRequestsSocketIo)
96 |
97 | // TODO add more socketio code for verifying authentication
98 |
99 | if (!module.parent) {
100 | // Start the server
101 | server.listen(PORT, (err) => {
102 | if (err) {
103 | console.log(err)
104 | return
105 | }
106 | console.log(`===> 🌎 Express Server started on port: ${PORT}!`)
107 | })
108 | }
109 |
110 | module.exports = app
111 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/client/components/NextBestActions/NextBestActions.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import * as styles from "./NextBestActions.module.scss";
4 | import * as widgetStyles from "@client/widget.module.scss";
5 | import {useTranslation} from "react-i18next";
6 | import {useEffect, useState} from "react";
7 | import {SocketPayload, useSocketEvent, useSocket} from "@client/providers/Socket";
8 | import BestAction from "./BestAction";
9 | import * as _ from "lodash";
10 | import {InlineLoading} from "@carbon/react";
11 |
12 | export enum ActionState {
13 | active = "active",
14 | stale = "stale",
15 | expired = "expired",
16 | complete = "complete"
17 | }
18 |
19 | export type Action = {
20 | text: string;
21 | actionId: number;
22 | state: ActionState;
23 | createdAt: number
24 | }
25 |
26 | const NextBestActions = () => {
27 | const [actions, setActions] = useState([]);
28 | const {t} = useTranslation();
29 | const {lastMessage} = useSocketEvent('celeryMessage');
30 | const {socket} = useSocket();
31 | const [sessionId, setSessionId] = useState();
32 |
33 | useEffect(() => {
34 | if (lastMessage) {
35 | const payload: SocketPayload = JSON.parse(lastMessage?.payloadString);
36 | console.log(payload)
37 |
38 | const action: Action = {
39 | text: payload?.parameters?.text || "",
40 | actionId: payload?.parameters?.action_id || 0,
41 | state: ActionState.active,
42 | createdAt: new Date().getTime()
43 | };
44 |
45 | if (payload?.type === "new_action") {
46 | setActions(prevState => [...prevState, action]);
47 | } else if (payload?.type === "session_started") {
48 | // trying to grab the session ID when receiving the session open message
49 | // we need this along with the agent id when sending an manual action on click message back to socketio
50 | setSessionId(payload.parameters.session_id)
51 | } else if (payload?.type === "completed_action") {
52 | action.state = ActionState.complete;
53 | updateAction(action);
54 | // const payload = {
55 | // destination: `agent-assist/${session_id}/ui`,
56 | // text: "Next step"
57 | // }
58 | // socket.emit("webUiMessage", JSON.stringify(payload))
59 | }
60 | }
61 | }, [lastMessage])
62 |
63 | const updateAction = (action: Action) => {
64 | setActions(prevState => {
65 | const actionToUpdate: Action | undefined = _.find(prevState, value => value.actionId === action?.actionId);
66 |
67 | if (actionToUpdate) {
68 | actionToUpdate.state = action.state;
69 | actionToUpdate.text = action.text;
70 | }
71 |
72 | return prevState;
73 | });
74 | };
75 |
76 | // this emits a message back to api-server, which then creates a celery task
77 | const sendManualCompletion = () => {
78 | const payload = {
79 | destination: `agent-assist/${sessionId}/ui`,
80 | text: "Next step"
81 | }
82 | console.log(payload)
83 | socket.emit("webUiMessage", JSON.stringify(payload))
84 | }
85 |
86 | return (
87 |
88 |
89 | {t("nextBestAction")}
90 |
91 |
92 | {actions.length ? actions.map((action, id) =>
93 | ) :
94 | }
95 |
96 |
97 | );
98 | };
99 |
100 | export default NextBestActions;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Agent Assist
2 |
3 | ## Overview
4 | Agent Assist is an asset to support the Active Listening Agent Assist usecase. Active Listening leverages watsonx.ai to provide functionality like Next Best Action, Call Sentiment, and Call Summarization to support human agents in real time to rapidly resolve caller issues. This asset provides a suite of microservices deployed as docker containers that orchestrate between a telephony provider like Genesys and IBM SaaS software including watsonx.ai, watsonx Assistant, Watson Discovery, Speech to Text, and IBM Voice Gateway.
5 |
6 | ## Installation
7 |
8 | These instructions assume that you are using (rancher desktop)[https://rancherdesktop.io/] (which supports docker compose).
9 |
10 | 1. git clone
11 | 2. `cp .env_example .env`
12 | 3. Edit the env file and fill in credentials under the required sections
13 | 4. install [Tilt CLI](https://tilt.dev/) using brew `brew install tilt`
14 | 6. run `tilt up`
15 | 7. Open the console by pressing space
16 | 8. Navigate to http://localhost:3003
17 | 9. Supply sample audio file in the top right corner (a sample audio file can be found under ./test/call-samples)
18 |
19 | ## Call Sample Files
20 | This repository provides a Watson STT Stream connector that simulates a Genesys CCaaS adapter (Call Center as a Service). Call sample files can be created by the mono-to-stereo-wav-converter inside ./utilities. Sample files will be provided soon!
21 |
22 | ## Using Without Tilt CLI
23 |
24 | For docker compose:
25 | ```sh
26 | docker-compose -f docker-compose.yml -f docker-compose.telemetry.yml -f docker-compose.ui.yml --env-file .env up
27 | ```
28 |
29 | For podman compose:
30 | ```sh
31 | podman compose -f docker-compose.yml -f docker-compose.telemetry.yml -f docker-compose.ui.yml --env-file .env up
32 | ```
33 |
34 | ## Localhost Ports
35 |
36 | | Service | Description | Port(s) |
37 | |------------------|----------------------|---------|
38 | | API-Server | Socketio Admin | 8000 |
39 | | wrapper-ui | AA Demo UI | 3003 (docker-compose), 5173 (dev mode) |
40 | | agent-dashboard-ui | Agent iframe | 3000 (docker-compose) |
41 | | stream connector | genesys | 8080 |
42 | | celery | worker | No ports|
43 | | celery flower | celery admin UI | 5555 |
44 | | jaeger | Jaeger OTEL UI | 16686 |
45 |
46 | ## Architecture
47 |
48 | 
49 |
50 | ### Celery/RabbitMQ/Redis
51 |
52 | [Python Celery](https://docs.celeryq.dev/en/stable/getting-started/first-steps-with-celery.html) is a distributed task queue system that sequences longer-running async tasks (like calling LLMs). Each turn of the agent/customer conversation (decoded by Watson STT) produces an Event which is packaged as a Celery Task (dispatcher task). It queues up a sequence of tasks to be executed. The tasks can land across different pods in the Celery cluster.
53 |
54 | 
55 |
56 | This Agent Assist solution uses RabbitMQ as the Celery Transport, and Redis as the Results backend.
57 |
58 | ### Socket.IO
59 |
60 | [Socket.io](https://socket.io/docs/v4/tutorial/introduction) is used for real-time communication between the Agent's web UI (agent-dashboard-ui) to the api-server (which contains a socket.io server). When each Celery task finishes, it typically has a step in which it emits a socket.io message to the server. Each agent is effectively inside a socket.io chatroom, and the Celery tasks emit messages into the chatroom (joined only by the agent) so that messages can be isolated to a per-agent basis.
61 |
62 | For bi-directional communication between
63 |
64 | ## Contributors
65 |
66 | - [Brian Pulito](https://github.com/bpulito)
67 | - [Kyle Sava](https://github.com/kylesava)
68 | - [Bob Fang](https://github.com/bobfang)
69 | - Keith Frost
70 |
71 |
--------------------------------------------------------------------------------
/celery/BaseAgent/BaseTask.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 | import socketio
3 | import redis
4 | import os
5 | import json
6 |
7 | from celery import Task
8 |
9 |
10 | class BaseTask(Task):
11 | _sio = None
12 | _redis_client = None
13 | _sio_status = False
14 |
15 | @property
16 | def sio(self):
17 | if self._sio is None:
18 | self._sio = socketio.Client(logger=True, engineio_logger=True)
19 | self._sio.connect(
20 | os.getenv("ANN_SOCKETIO_SERVER", "http://localhost:8000"),
21 | namespaces=["/celery"],
22 | )
23 | print("Socketio client initialized")
24 | return self._sio
25 |
26 | @property
27 | def redis_client(self):
28 | if self._redis_client is None:
29 | self._redis_client = redis.StrictRedis(
30 | host=os.getenv("AAN_REDIS_HOST", "localhost"),
31 | port=os.getenv("AAN_REDIS_PORT", 6379),
32 | db=os.getenv("AAN_REDIS_DB_INDEX", 2),
33 | )
34 | print("Starting Redis client")
35 | return self._redis_client
36 |
37 | def create_json(self, key, value):
38 | json_value = json.dumps(value)
39 | self._redis_client.set(key, json_value)
40 |
41 | def read_json(self, key):
42 | json_value = self._redis_client.get(key)
43 | if json_value:
44 | return json.loads(json_value)
45 | return None
46 |
47 | def update_json(self, key, value):
48 | json_value = json.dumps(value)
49 | if self._redis_client.exists(key):
50 | self._redis_client.set(key, json_value)
51 | else:
52 | raise KeyError(f"Key '{key}' does not exist in Redis")
53 |
54 | def delete(self, key):
55 | if self._redis_client.exists(key):
56 | self._redis_client.delete(key)
57 | else:
58 | raise KeyError(f"Key '{key}' does not exist in Redis")
59 |
60 | def append_to_list_json(self, key, value):
61 | """
62 | Append a value to the list stored at the given key.
63 | If the key does not exist, a new list is created.
64 | """
65 | json_value = json.dumps(value)
66 | self._redis_client.rpush(key, json_value)
67 |
68 | def get_list_len(self, key):
69 | """
70 | Returns length using LLEN for a key. Returns 0 when the key doesn't exist
71 | """
72 | return self._redis_client.llen(key)
73 |
74 | def extract_client_id(self, topic):
75 | """
76 | Get the client_id from an agent assist topic
77 | """
78 | # Split the input string by '/'
79 | parts = topic.split('/')
80 |
81 | # Check if there are at least three parts (two slashes)
82 | if len(parts) >= 3:
83 | # Return the string between the first and second slashes
84 | return parts[1]
85 | else:
86 | # If no UUID is found, return None
87 | return None
88 |
89 | def extract_event(self, topic):
90 | """
91 | Get the client_id from an agent assist topic
92 | """
93 | # Split the input string by '/'
94 | parts = topic.split('/')
95 |
96 | # Check if there are at least three parts (two slashes)
97 | if len(parts) >= 3:
98 | # Return the string between the first and second slashes
99 | return parts[2]
100 | else:
101 | # If no event is found, return None
102 | return None
103 |
104 | def extract_agent_id(self, message):
105 | """
106 | Get the agent_id from an agent assist message
107 | """
108 | try:
109 | message_data = json.loads(message)
110 | agent_id = message_data.get("agent_id", "")
111 | return agent_id
112 | except (json.JSONDecodeError, AttributeError):
113 | return None
--------------------------------------------------------------------------------
/celery/aan_extensions/SummaryAgent/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 | from celery_worker import app
3 | from BaseAgent import BaseTask
4 | from opentelemetry import trace
5 | from opentelemetry.trace import SpanKind
6 | from .summ import summarize
7 | import logging
8 | import json
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class colors:
14 | OKGREEN = "\033[92m"
15 | OKBLUE = "\033[94m"
16 | ENDC = "\033[0m"
17 |
18 |
19 | # WORK IN PROGRESS - PLACEHOLDER
20 | @app.task(base=BaseTask.BaseTask, bind=True)
21 | def process_transcript(self, topic, message):
22 | with trace.get_tracer(__name__).start_as_current_span(
23 | "process_transcript", kind=SpanKind.PRODUCER
24 | ) as span:
25 | result = topic + "---" + message
26 | # adjusted_sleep_time = result * 2 / 1000 # Convert total to seconds and double it
27 | # # Simulate a blocking wait
28 | # time.sleep(adjusted_sleep_time)
29 |
30 | print(
31 | f"SummaryAgent {colors.OKGREEN}{topic}{colors.ENDC} + {colors.OKBLUE}{message}{colors.ENDC}"
32 | )
33 | # emit(event, data=None, room=None, skip_sid=None, namespace=None)
34 | print(self.sio)
35 | try:
36 | # self.sio.emit('celeryMessage', {'payloadString': message, 'destinationName': topic}, namespace='/celery') #
37 | client_id = self.extract_client_id(topic)
38 | print(f"client_id: {client_id}")
39 | message_data = json.loads(message)
40 | with trace.get_tracer(__name__).start_as_current_span(
41 | "redis_op"):
42 | if client_id: #must have client_id, otherwise it is a session_start or end
43 | turns_counter = self.redis_client.llen(client_id) or 0
44 | print(f"Turns counter: {turns_counter}")
45 | if (turns_counter != 0) and (turns_counter % 2 == 0):
46 | transcripts_obj = self.redis_client.lrange(client_id, 0, -1) # returns a list
47 | # {"source":"internal","text":"example"}
48 | transcripts_dicts = [json.loads(item) for item in transcripts_obj]
49 | transcription_text = "\n".join(
50 | f"{'Agent' if item['source'] == 'internal' else 'Customer'}: {item['text']}"
51 | for item in transcripts_dicts
52 | )
53 | with trace.get_tracer(__name__).start_as_current_span(
54 | "summarize"):
55 | new_summary = summarize(transcription_text)
56 |
57 | if new_summary:
58 | summary_topic = f"agent-assist/{client_id}/summarization"
59 | summary_message = json.dumps(
60 | {
61 | "type": "summary",
62 | "parameters": {"text": new_summary, "final": False},
63 | }
64 | )
65 | try:
66 | self.sio.emit(
67 | "celeryMessage",
68 | {
69 | "payloadString": summary_message,
70 | "destinationName": summary_topic,
71 | 'agent_id': message_data['agent_id']
72 | },
73 | namespace="/celery",
74 |
75 | )
76 | except Exception as e:
77 | print(f"Error publishing extracted entities: {e}")
78 | except Exception as e:
79 | print(e)
80 | # the return result is stored in the celery backend
81 | return result
82 |
--------------------------------------------------------------------------------
/api-server/oidc/passportConfig.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const JwtStrategy = require('passport-jwt').Strategy
4 | const ExtractJwt = require('passport-jwt').ExtractJwt
5 | const jwksClient = require('jwks-rsa')
6 | const debug = require('debug')('passportConfig')
7 |
8 | /**
9 | * Returns a configured Passport with JWT/OIDC strategy
10 | * @param {any} passport Passport Library
11 | * @param {boolean} oidcEnabled if true the authentication middleware will run passport.authenticate
12 | * @param {string} oidcIssuer location of the OIDC issuer
13 | * @param {string} oidcJwksUri location of the OIDC JWK public key set
14 | * @returns {configuredPassport, authenticateRequests} configuredPassport object and authenticateRequests express middleware
15 | **/
16 | exports.configurePassportJwt = function(passport, oidcEnabled, oidcIssuer, oidcJwksUri) {
17 |
18 | const jwtOptions = {
19 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
20 | secretOrKeyProvider: jwksClient.passportJwtSecret({
21 | jwksUri: `${oidcJwksUri}`,
22 | cache: true,
23 | rateLimit: true,
24 | jwksRequestsPerMinute: 5,
25 | }),
26 | issuer: `${oidcIssuer}`,
27 | //algorithms: ['RS256'],
28 | };
29 | passport.use(new JwtStrategy(jwtOptions, (payload, done) => {
30 | debug(`inside jwt strategy`)
31 | debug(payload)
32 | const user = {
33 | id: payload.sub,
34 | //username: payload.preferred_username,
35 | email: payload.email,
36 | };
37 | // Pass the user object to the next middleware
38 | return done(null, user);
39 | }));
40 |
41 | /**
42 | * Express Middleware for calling passport.authenticate using JWT
43 | * @param {any} req
44 | * @param {any} res
45 | * @param {any} next
46 | * @returns {any}
47 | */
48 | function authenticateRequests(req, res, next) {
49 | if (oidcEnabled) {
50 | debug(`checking auth for request`);
51 | passport.authenticate('jwt', { session: false }, (err, user, info) => {
52 | debug(`passport trying to auth`)
53 | if (err) {
54 | debug(err)
55 | // Handle authentication error
56 | return next(err);
57 | }
58 | if (!user) {
59 | // Authentication failed
60 | debug(`passport auth unsuccessful`)
61 | return res.status(401).send('Unauthorized');
62 | }
63 | // Authentication successful
64 | debug(`passport auth successful`)
65 | req.user = user; // Attach user to request object
66 | return next();
67 | })(req, res, next);
68 | } else {
69 | next();
70 | }
71 | }
72 |
73 | /**
74 | * Express Middleware for calling passport.authenticate using JWT to be used by socketio
75 | * @param {any} req
76 | * @param {any} res
77 | * @param {any} next
78 | * @returns {any}
79 | */
80 | function authenticateRequestsSocketIo(req, res, next) {
81 | if (oidcEnabled) {
82 | debug(`checking auth for io request`);
83 | debug(req.headers)
84 | passport.authenticate('jwt', { session: false }, (err, user, info) => {
85 | debug(`passport trying to io auth`)
86 | if (err) {
87 | debug(err)
88 | // Handle authentication error
89 | return next(err);
90 | }
91 | if (!user) {
92 | // Authentication failed
93 | debug(`passport io auth unsuccessful`)
94 | return next(new Error('Failed to authenticate token'));
95 | }
96 | // Authentication successful
97 | debug(`passport io auth successful`)
98 | req.user = user; // Attach user to request object
99 | return next();
100 | })(req, res, next);
101 | } else {
102 | next();
103 | }
104 | }
105 |
106 | return { configuredPassport:passport, authenticateRequests , authenticateRequestsSocketIo}
107 | }
108 |
109 |
110 | // Use the JwtStrategy with additional validation
111 |
112 |
113 |
114 | // app.use(passport.initialize())
115 |
116 |
117 |
--------------------------------------------------------------------------------
/agent-dashboard-ui/src/client/components/ExtractedEntities/ExtractedEntities.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import {
4 | DataTable,
5 | DataTableSkeleton,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableHeader,
11 | TableRow
12 | } from '@carbon/react';
13 | import * as styles from "./ExtractedEntities.module.scss";
14 | import * as widgetStyles from "@client/widget.module.scss";
15 | import {useTranslation} from "react-i18next";
16 | import {useEffect, useState} from "react";
17 | import {v4 as uuid} from 'uuid';
18 | import {SocketPayload, useSocketEvent} from "@client/providers/Socket";
19 | import * as _ from "lodash";
20 |
21 | type RowType = {
22 | id: string;
23 | name: string | undefined;
24 | values: Set;
25 | }
26 |
27 | const ExtractedEntities = () => {
28 | const [rows, setRows] = useState([]);
29 | const {t} = useTranslation();
30 |
31 | const headers = [
32 | {
33 | key: 'name',
34 | header: t("entityName"),
35 | },
36 | {
37 | key: 'values',
38 | header: t("entityValues"),
39 | },
40 | ];
41 |
42 | const {lastMessage} = useSocketEvent('celeryMessage')
43 |
44 | useEffect(() => {
45 | if (lastMessage) {
46 | const payload: SocketPayload = JSON.parse(lastMessage?.payloadString);
47 |
48 | if (payload?.type === "extraction") {
49 | if (payload?.parameters?.value !== "[None]") {
50 | const newRow: RowType = {
51 | id: uuid(),
52 | name: payload?.parameters?.title,
53 | values: new Set([payload?.parameters?.value])
54 | };
55 |
56 | setRows((prev) => {
57 | const i = _.findIndex(prev, ["name", newRow.name]);
58 |
59 | /*
60 | if (i > -1) {
61 | prev[i]?.values?.add(newRow?.values?.values().next().value)
62 | } else {
63 | prev = [...prev, newRow];
64 | }
65 | */
66 | if (i == -1) {
67 | prev = [...prev, newRow];
68 | }
69 |
70 | return prev;
71 | });
72 | }
73 | }
74 | }
75 | }, [lastMessage])
76 |
77 | const buildRow = (row: any, getRowProps: Function) => {
78 | const values = [...row.cells.find((el: any) => el.info.header === "values")?.value];
79 | return
80 | {row.cells.map((cell: any) => (
81 |
82 | {cell.info.header === "values" ? values.map((val: string) => <>{val} >) : cell.value}
83 |
84 | ))}
85 |
86 | };
87 |
88 | return (
89 |
90 |
91 | {t("extractedEntities")}
92 |
93 | {rows?.length ?
94 | {({rows, headers, getTableProps, getHeaderProps, getRowProps, getExpandedRowProps}) => (
95 |
96 |
97 |
98 | {headers.map((header, id) => (
99 |
100 | {header.header}
101 |
102 | ))}
103 |
104 |
105 |
106 | {rows.map((row) => buildRow(row, getRowProps))}
107 |
108 |
109 | )}
110 | :
111 |
117 |
118 | }
119 |
120 | );
121 | };
122 |
123 | export default ExtractedEntities;
--------------------------------------------------------------------------------
/celery/aan_extensions/ExtractionAgentOld/entity.py:
--------------------------------------------------------------------------------
1 | from ibm_watson_machine_learning.foundation_models import Model
2 | from ibm_watson_machine_learning.metanames import GenTextParamsMetaNames as GenParams
3 | import os
4 | import logging
5 | import re
6 |
7 | logging.basicConfig(level=logging.INFO)
8 |
9 | DETAIL = 0.5
10 | MAX_NEW_TOKENS = 500
11 | TOKEN_LIMIT = 1024
12 | # Initialize the model
13 | generate_params = {GenParams.MAX_NEW_TOKENS: MAX_NEW_TOKENS}
14 | model_name = os.environ.get('AAN_ENTITY_EXTRACTION_LLM_MODEL_NAME', 'ibm/granite-13b-chat-v2')
15 | logging.info(f'LLM model name to be used for entity extraction: {model_name}')
16 | model = Model(
17 | model_id=model_name,
18 | params=generate_params,
19 | credentials={
20 | "apikey": os.environ.get('AAN_WML_APIKEY'),
21 | "url": os.environ.get('AAN_WML_URL', "https://us-south.ml.cloud.ibm.com")
22 | },
23 | project_id=os.environ.get('AAN_WML_PROJECT_ID')
24 | )
25 |
26 | def extract_entities(message):
27 | """
28 | Extracts entities from the given message using the LLM model, following the specified prompt instructions.
29 |
30 | :param message: The customer message from which to extract entities.
31 | :return: Extracted entities in the required format or "none" if no entities were found.
32 | """
33 | try:
34 | entity_extraction_prompt = entity_extraction_prompt = entity_extraction_prompt = """
35 | [INST] <>
36 | You are an assistant with the task of precisely identifying and listing specific entities from texts. Focus on extracting: street address, city, state, zipcode, phone number, person's name, email address, and issues described. Your responses must be direct, only including detected entities with their labels, or "None" if no relevant entities are found.
37 |
38 | - Be concise and direct, avoiding additional context or narrative.
39 | - If an entity type is not mentioned, do not infer it.
40 | - Ensure accuracy and relevancy in your responses, remaining neutral and unbiased.
41 |
42 | The following are example instructions for clarity:
43 | - "Phone number: [number]" if a phone number is mentioned.
44 | - "Issue: [brief issue description]" for any issues described.
45 | - "None" if no relevant entities are found in the text.
46 |
47 | Using this guidance, extract entities from the conversation excerpt below, adhering strictly to the outlined instructions and examples. Your role is to provide helpful, accurate, and straightforward entity identifications based on the text.
48 | < >[INST]
49 | {}
50 | [/INST]
51 | """.format(message)
52 |
53 | extraction_response = model.generate_text(prompt=entity_extraction_prompt)
54 | extracted_text = extraction_response if isinstance(extraction_response, str) else extraction_response.get('generated_text', '')
55 | entities = parse_llm_response(extracted_text)
56 | return entities
57 | except Exception as e:
58 | logging.error(f"Error during entity extraction: {e}")
59 | return {}
60 |
61 | def process_message(transcript):
62 | """
63 | Processes the transcript message to extract entities.
64 |
65 | :param transcript: The full transcript or latest customer message.
66 | :return: The extracted entities or indication of none found.
67 | """
68 | extracted_entities = extract_entities(transcript)
69 | return extracted_entities
70 |
71 |
72 | def parse_llm_response(response_text):
73 | """
74 | Parse the LLM response to extract entities and their values.
75 | Dynamically accepts any entity titles provided by the LLM.
76 | Returns a dictionary of the entities with their extracted values or 'None'.
77 | """
78 |
79 | entities = {}
80 |
81 | entity_regex = re.compile(r"(?P[^:\n]+):\s*(?P.+?)(?=\n[^:\n]+:|$)", re.DOTALL)
82 |
83 | matches = entity_regex.finditer(response_text)
84 |
85 | for match in matches:
86 | entity = match.group("entity").strip()
87 | entity = re.sub(r"^[^a-zA-Z]+", "", entity)
88 | value = match.group("value").strip()
89 | value = None if value.lower() == "none" else value
90 |
91 | # Add or update the entity in the dictionary
92 | entities[entity] = value
93 |
94 | return entities
95 |
96 |
--------------------------------------------------------------------------------