├── celery ├── BaseAgent │ ├── __init__.py │ ├── ConfigCache.py │ ├── BaseTask test.py │ └── BaseTask.py ├── aan_extensions │ ├── CacheAgent │ │ ├── __init__.py │ │ └── tasks.py │ ├── SentimentAgent │ │ ├── __init__.py │ │ └── sentiment.py │ ├── SummaryAgent │ │ ├── __init__.py │ │ └── tasks.py │ ├── DispatcherAgent │ │ ├── __init__.py │ │ └── tasks.py │ ├── ExtractionAgent │ │ └── __init__.py │ ├── ExtractionAgentOld │ │ ├── __init__.py │ │ └── entity.py │ ├── NextBestActionAgent │ │ └── __init__.py │ └── TranscriptionAgent │ │ ├── __init__.py │ │ └── tasks.py ├── README.MD ├── Dockerfile ├── swarmLauncher.py ├── requirements.txt ├── celery_worker.py └── old-tasks.py ├── wrapper-ui ├── server │ ├── utils │ │ ├── uploadRouter.js │ │ ├── websocketRouter.js │ │ ├── authRouter.js │ │ ├── listBuckets.js │ │ └── App-ID.html │ ├── config │ │ └── localdev-config.json │ ├── .env-example │ ├── package.json │ └── app-basic.js ├── src │ ├── App.css │ ├── index.css │ ├── assets │ │ └── ibm-50.png │ ├── components │ │ ├── Dashboard │ │ │ ├── TopBar │ │ │ │ └── TopBar.jsx │ │ │ ├── RightBar │ │ │ │ └── RightBar.jsx │ │ │ ├── MiddleBox │ │ │ │ └── MiddleBox.jsx │ │ │ ├── ActiveSessionBar │ │ │ │ └── ActiveSessionBar.jsx │ │ │ ├── Conversation │ │ │ │ ├── Message │ │ │ │ │ └── Message.jsx │ │ │ │ └── Conversation.jsx │ │ │ ├── FileStreamer │ │ │ │ └── FileStreamer.jsx │ │ │ ├── WatsonAssistant │ │ │ │ └── WatsonAssistant.jsx │ │ │ ├── SessionBar │ │ │ │ └── SessionBar.jsx │ │ │ ├── SessionUser │ │ │ │ └── SessionUser.jsx │ │ │ └── SentimentProgress │ │ │ │ └── SentimentProgress.jsx │ │ └── Modal │ │ │ └── Setting │ │ │ └── SettingModal.jsx │ ├── utils │ │ └── data.js │ ├── pages │ │ └── Dashboard │ │ │ └── Dashboard.jsx │ ├── main.jsx │ ├── hooks │ │ ├── useAuthenticatedSocket.js │ │ └── useAgentIdProvider.jsx │ ├── libs │ │ └── Mqtt │ │ │ └── MqttMethods.js │ └── App.jsx ├── public │ ├── joy.png │ ├── anger.png │ ├── fear.png │ ├── neutral.png │ ├── sadness.png │ ├── watsonx.png │ ├── disgusting.png │ ├── folder-icon.png │ └── logo-ibm-square.png ├── postcss.config.js ├── landing │ ├── images │ │ ├── logo-ibm-square.png │ │ └── app_id_icon.svg │ ├── index.html │ └── stylesheets │ │ └── index.css ├── tailwind.config.js ├── .dockerignore ├── .gitignore ├── example-Dockerfile-nginx ├── index.html ├── .eslintrc.cjs ├── .env-example ├── Dockerfile ├── package.json └── vite.config.js ├── agent-dashboard-ui ├── .dockerignore ├── src │ ├── @types │ │ ├── declaration.d.ts │ │ ├── i18next.d.ts │ │ ├── index.d.ts │ │ └── widget.ts │ ├── client │ │ ├── components │ │ │ ├── Dashboard │ │ │ │ ├── Dashboard.module.scss │ │ │ │ └── Dashboard.tsx │ │ │ ├── CallSummary │ │ │ │ ├── CallSummary.module.scss │ │ │ │ └── CallSummary.tsx │ │ │ ├── ExtractedEntities │ │ │ │ ├── ExtractedEntities.module.scss │ │ │ │ └── ExtractedEntities.tsx │ │ │ ├── WatsonxAssistant │ │ │ │ ├── WatsonxAssistant.module.scss │ │ │ │ └── WatsonxAssistant.tsx │ │ │ ├── NextBestActions │ │ │ │ ├── NextBestActions.module.scss │ │ │ │ ├── BestAction.module.scss │ │ │ │ ├── BestAction.tsx │ │ │ │ └── NextBestActions.tsx │ │ │ └── App │ │ │ │ └── App.tsx │ │ ├── providers │ │ │ ├── AppProvider.tsx │ │ │ ├── EnvVars.tsx │ │ │ └── Socket.tsx │ │ ├── index.html │ │ ├── widget.module.scss │ │ ├── locales │ │ │ └── en.json │ │ ├── index.scss │ │ ├── i18n.ts │ │ └── index.tsx │ └── server │ │ ├── websockets.ts │ │ ├── telemetry.ts │ │ ├── routes │ │ └── environment.ts │ │ ├── index.ts │ │ └── auth.ts ├── tslint.json ├── README.md ├── .env-example ├── jest.config.js ├── Dockerfile ├── tsconfig.json ├── .gitignore └── package.json ├── api-server ├── .dockerignore ├── public │ ├── favicon.png │ ├── img │ │ ├── logo-dark.3727fec5.svg │ │ └── logo-light.73342c25.svg │ ├── css │ │ └── app.6e3b9661.css │ └── index.html ├── .env.example ├── CHANGELOG.md ├── middlewares │ ├── errorHandler.js │ ├── notFound.js │ ├── index.js │ └── fileHandler.js ├── celery │ └── celeryClient.js ├── utils │ ├── config.js │ ├── helpers.js │ └── data.js ├── .gitignore ├── debugwss │ └── index.js ├── Dockerfile ├── package.json ├── socketio │ └── configurePool.js ├── index.js └── oidc │ └── passportConfig.js ├── images ├── aa-arch.png └── celery-fan.png ├── watson-stt-stream-connector ├── .dockerignore ├── wrapper.js ├── app.js ├── lib │ ├── wsSpan.js │ ├── EventPublisher.js │ ├── SpeechToTextEngine.js │ ├── spanUtils.js │ ├── StreamingSessionState.js │ ├── setupTelemetry.js │ ├── GenesysStreamingSessionState.js │ ├── SiprecStreamingSessionState.js │ ├── MonoChannelStreamingSessionState.js │ ├── CeleryEventPublisher.js │ ├── MQTTEventPublisher.js │ ├── WatsonSpeechToTextEngine.js │ └── StreamConnectorServer.js ├── .eslintrc ├── .editorconfig ├── Dockerfile ├── .gitignore ├── package.json └── .env-example ├── utilities ├── README.md └── mono-to-stereo-wav-converter │ ├── .env-example │ ├── package.json │ ├── README.md │ ├── wav_header_dump.js │ └── .gitignore ├── .github └── dco.yml ├── web-stream-client ├── README.md ├── test.html └── simple_websocket_server.py ├── Tiltfile ├── package.json ├── docker-compose.ui.yml ├── docker-compose.telemetry.yml ├── license-check-and-add.config.json ├── .env_example ├── docker-compose.config.yml ├── .gitignore ├── CONTRIBUTING.md └── README.md /celery/BaseAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wrapper-ui/server/utils/uploadRouter.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-dashboard-ui/.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log -------------------------------------------------------------------------------- /celery/aan_extensions/CacheAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/SentimentAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/SummaryAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/DispatcherAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/ExtractionAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/ExtractionAgentOld/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/NextBestActionAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /celery/aan_extensions/TranscriptionAgent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api-server/.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | build/ 4 | .env -------------------------------------------------------------------------------- /agent-dashboard-ui/src/@types/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.scss"; -------------------------------------------------------------------------------- /celery/README.MD: -------------------------------------------------------------------------------- 1 | ## Startup 2 | 3 | ```sh 4 | python swarmLauncher.py 5 | ``` -------------------------------------------------------------------------------- /wrapper-ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /images/aa-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/images/aa-arch.png -------------------------------------------------------------------------------- /watson-stt-stream-connector/.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | build/ 4 | .env 5 | 6 | -------------------------------------------------------------------------------- /images/celery-fan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/images/celery-fan.png -------------------------------------------------------------------------------- /wrapper-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /wrapper-ui/public/joy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/joy.png -------------------------------------------------------------------------------- /wrapper-ui/public/anger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/anger.png -------------------------------------------------------------------------------- /wrapper-ui/public/fear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/fear.png -------------------------------------------------------------------------------- /agent-dashboard-ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /api-server/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/api-server/public/favicon.png -------------------------------------------------------------------------------- /wrapper-ui/public/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/neutral.png -------------------------------------------------------------------------------- /wrapper-ui/public/sadness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/sadness.png -------------------------------------------------------------------------------- /wrapper-ui/public/watsonx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/watsonx.png -------------------------------------------------------------------------------- /wrapper-ui/public/disgusting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/disgusting.png -------------------------------------------------------------------------------- /wrapper-ui/public/folder-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/folder-icon.png -------------------------------------------------------------------------------- /wrapper-ui/src/assets/ibm-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/src/assets/ibm-50.png -------------------------------------------------------------------------------- /utilities/README.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | 3 | ## Overview 4 | Contains various utilities for working with the agent assist platform. -------------------------------------------------------------------------------- /watson-stt-stream-connector/wrapper.js: -------------------------------------------------------------------------------- 1 | require('@opentelemetry/auto-instrumentations-node').register(); 2 | require('./app.js'); -------------------------------------------------------------------------------- /wrapper-ui/public/logo-ibm-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/public/logo-ibm-square.png -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/Dashboard/Dashboard.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .dashboard { 4 | margin-bottom: 4px; 5 | } 6 | -------------------------------------------------------------------------------- /wrapper-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /wrapper-ui/landing/images/logo-ibm-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/agent-assist/main/wrapper-ui/landing/images/logo-ibm-square.png -------------------------------------------------------------------------------- /watson-stt-stream-connector/app.js: -------------------------------------------------------------------------------- 1 | 2 | // import env 3 | require('dotenv').config() 4 | 5 | require('./lib/StreamConnectorServer').start(); 6 | -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | # This enables DCO bot for you, please take a look https://github.com/probot/dco 2 | # for more details. 3 | require: 4 | members: false -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/CallSummary/CallSummary.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .summaryText { 4 | pre { 5 | white-space: pre-wrap; 6 | } 7 | } -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/ExtractedEntities/ExtractedEntities.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .tableCell { 4 | vertical-align: top !important; 5 | } -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/WatsonxAssistant/WatsonxAssistant.module.scss: -------------------------------------------------------------------------------- 1 | .summaryText { 2 | pre { 3 | white-space: pre-wrap; 4 | } 5 | } -------------------------------------------------------------------------------- /agent-dashboard-ui/src/@types/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import "i18next"; 2 | 3 | declare module "i18next" { 4 | interface CustomTypeOptions { 5 | returnNull: false 6 | } 7 | } -------------------------------------------------------------------------------- /api-server/.env.example: -------------------------------------------------------------------------------- 1 | 2 | # this file is provided for local development for the API server 3 | 4 | # nothing needs to be set 5 | #OIDC_ISSUER= 6 | #OIDC_ISSUER_JWKS_URI= -------------------------------------------------------------------------------- /utilities/mono-to-stereo-wav-converter/.env-example: -------------------------------------------------------------------------------- 1 | # Watson STT 2 | WATSON_STT_URL=Watson STT URL 3 | WATSON_API_KEY=Watson STT apikey 4 | WATSON_STT_MODEL=Watson STT model -------------------------------------------------------------------------------- /agent-dashboard-ui/README.md: -------------------------------------------------------------------------------- 1 | # Agent Dashboard (iframe target) 2 | 3 | ## Overview 4 | 5 | - Call Summary 6 | 7 | - Extracted Entities 8 | 9 | - Next Best Actions 10 | 11 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/NextBestActions/NextBestActions.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | @use '@carbon/themes'; 4 | 5 | .actionTileContainer { 6 | overflow-y: scroll; 7 | } 8 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | 4 | declare global { 5 | interface Window { 6 | watsonAssistantChatOptions: any; 7 | watsonAssistantChatInstance: any; 8 | } 9 | } -------------------------------------------------------------------------------- /api-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | -------------------------------------------------------------------------------- /agent-dashboard-ui/.env-example: -------------------------------------------------------------------------------- 1 | # see src/server/websockets.ts 2 | # 3 | 4 | OTEL_SERVICE_NAME=agent-dashboard-ui 5 | PORT=3000 6 | ANN_SOCKETIO_SERVER=http://localhost:8000 7 | ANN_WRAPPER_DASHBOARD=http://localhost:3003 8 | -------------------------------------------------------------------------------- /api-server/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | 2 | // Error Handler 3 | 4 | module.exports = (err, req, res, next) => { 5 | res.status(err.status || err.statusCode || 500) 6 | return res.send({ 7 | errors: [err], 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /api-server/middlewares/notFound.js: -------------------------------------------------------------------------------- 1 | 2 | const createError = require('http-errors') 3 | 4 | // catch 404 and forward it to error handler 5 | module.exports = (req, res, next) => { 6 | const err = new createError.NotFound() 7 | return next(err) 8 | } 9 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/wsSpan.js: -------------------------------------------------------------------------------- 1 | const { startSpan } = require('./spanUtils'); 2 | 3 | // Start the parent span 4 | const parentSpan = startSpan('receive-audio-stream'); 5 | 6 | // Pass the parent span to other modules 7 | module.exports = { parentSpan }; -------------------------------------------------------------------------------- /api-server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | 2 | const errorHandler = require('./errorHandler') 3 | const notFound = require('./notFound') 4 | const fileHandler = require('./fileHandler') 5 | 6 | module.exports = { 7 | errorHandler, 8 | notFound, 9 | fileHandler, 10 | } 11 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "no-console": "error", 9 | "no-underscore-dangle": "off" 10 | }, 11 | "extends": "airbnb-base" 12 | } 13 | -------------------------------------------------------------------------------- /web-stream-client/README.md: -------------------------------------------------------------------------------- 1 | # web-stream-client 2 | 3 | ## Overview 4 | This javascript client is a widget that is designed to run in a browser. It is used to open a WAV file (in the following format: stereo, ulaw, 8 kHz) and stream it into the agent assist platform using the Genesys AudioHook websocket API. -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/providers/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {EnvVarsProvider} from "@client/providers/EnvVars"; 4 | import { IoProvider } from 'socket.io-react-hook'; 5 | 6 | export const AppProvider = (props: any) => ( 7 | {props.children} 8 | ); -------------------------------------------------------------------------------- /wrapper-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | flex: { 7 | 3: "0 0 33.333333%", 8 | 6: "0 0 66.666667%", 9 | }, 10 | }, 11 | }, 12 | plugins: [], 13 | }; 14 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/@types/widget.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const Widget = t.interface({ 4 | type: t.string, 5 | minColumns: t.number 6 | }); 7 | 8 | export const WidgetConfig = t.interface({ 9 | widgets: t.array(Widget) 10 | }); 11 | 12 | export type WidgetConfigT = t.TypeOf; // compile-time type -------------------------------------------------------------------------------- /api-server/celery/celeryClient.js: -------------------------------------------------------------------------------- 1 | const celery = require('celery-node'); 2 | 3 | const rabbitUrl = process.env.AAN_AMQP_URI || 'amqp://admin:adminpass@localhost:5672'; 4 | const redisUrl = process.env.AAN_REDIS_URI || 'redis://localhost:6379/1' 5 | 6 | const client = celery.createClient( 7 | rabbitUrl, redisUrl 8 | ); 9 | 10 | module.exports = client -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 | 10 |
Javascript is disabled
11 | 12 | -------------------------------------------------------------------------------- /api-server/utils/config.js: -------------------------------------------------------------------------------- 1 | 2 | const nconf = require('nconf') 3 | const config = nconf.env().get() 4 | 5 | // const requiredParams = ['PGRST_JWT_SECRET', 'PGRST_URL'] 6 | 7 | // requiredParams.forEach((key) => { 8 | // if (!config[key]) { 9 | // console.error(`Required parameter is missing: ${key}`) 10 | // process.exit(1) 11 | // } 12 | // }) 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /wrapper-ui/.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | server/node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? -------------------------------------------------------------------------------- /wrapper-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | server/node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /wrapper-ui/example-Dockerfile-nginx: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as builder 2 | 3 | # Create app directory 4 | WORKDIR /home/node/app 5 | 6 | # Install app dependencies 7 | COPY package*.json ./ 8 | 9 | RUN npm ci 10 | 11 | COPY . . 12 | 13 | RUN npm run build 14 | 15 | FROM nginx:alpine 16 | 17 | ENV NODE_ENV=testing 18 | 19 | COPY --from=builder /home/node/app/dist /usr/share/nginx/html 20 | 21 | EXPOSE 80 22 | 23 | -------------------------------------------------------------------------------- /celery/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | ENV NLTK_DATA=/usr/share/nltk_data 4 | 5 | # Install NLTK and other dependencies 6 | RUN pip install nltk 7 | 8 | # Create a directory for NLTK data and download the 'punkt' resource 9 | RUN mkdir -p $NLTK_DATA \ 10 | && python -m nltk.downloader punkt -d $NLTK_DATA 11 | 12 | 13 | WORKDIR /usr/src/app 14 | COPY . . 15 | RUN pip3 install -r requirements.txt 16 | 17 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/widget.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | @use '@carbon/type'; 4 | @use '@carbon/themes'; 5 | 6 | 7 | .dashboardWidget { 8 | background-color: themes.$layer-01; 9 | height: 480px; 10 | padding: 16px; 11 | margin-top: 16px; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .widgetTitle { 16 | @include type.type-style('productive-heading-01'); 17 | margin-bottom: 16px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/EventPublisher.js: -------------------------------------------------------------------------------- 1 | 2 | class EventPublisher { 3 | /* eslint-disable class-methods-use-this */ 4 | publish(topic, message, parentSpanCtx) {} 5 | 6 | /** 7 | * Destroys the Event Publisher if a close from the other side occurs 8 | */ 9 | // eslint-disable-next-line class-methods-use-this 10 | destroy() { 11 | throw new Error('not implemented'); 12 | } 13 | } 14 | module.exports = EventPublisher; -------------------------------------------------------------------------------- /wrapper-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Agent Assist 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /api-server/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | tmp/ 27 | -------------------------------------------------------------------------------- /web-stream-client/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Audio Streaming 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /api-server/debugwss/index.js: -------------------------------------------------------------------------------- 1 | const nconf = require('nconf') 2 | const config = nconf.env().get() 3 | const { Emitter } = require("@socket.io/postgres-emitter"); 4 | const { Pool } = require("pg"); 5 | 6 | console.log(config.SOCKETIO_DB_URI) 7 | 8 | const pool = new Pool({ 9 | connectionString: config.SOCKETIO_DB_URI, 10 | max: 1 11 | }); 12 | 13 | const io = new Emitter(pool); 14 | setInterval(() => { 15 | io.emit("ping", new Date()); 16 | }, 1000); -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "callSummary": "Call Summary", 3 | "clickToComplete": "Click the action to complete", 4 | "entityName": "Entity Name", 5 | "entityValues": "Entity Values", 6 | "extractedEntities": "Extracted Entities", 7 | "loadingAction": "Loading next best action", 8 | "loadingSummary": "Loading summary", 9 | "nextBestAction": "Next Best Action", 10 | "otherPotentialValues": "Other potential values: " 11 | } 12 | -------------------------------------------------------------------------------- /wrapper-ui/server/config/localdev-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "792bb9b5-e650-41fd-b919-8bdd4c491e73", 3 | "oauthServerUrl": "https://us-south.appid.cloud.ibm.com/oauth/v4/538e68a2-d74b-4a0f-a0e9-ab2d9ca658ed", 4 | "profilesUrl": "https://us-south.appid.cloud.ibm.com", 5 | "secret": "ZTBhODUxYjAtZDE1Yy00YThkLTk2MDEtMzQyZTc4NTY4YmFh", 6 | "tenantId": "538e68a2-d74b-4a0f-a0e9-ab2d9ca658ed", 7 | "redirectUri": "http://localhost:3000/ibm/cloud/appid/callback" 8 | } 9 | -------------------------------------------------------------------------------- /agent-dashboard-ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | collectCoverage: true, 4 | moduleFileExtensions: ["ts", "tsx", "js"], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | testMatch: ["**/test/unit/**/*.+(ts|tsx)"], 9 | moduleNameMapper: { 10 | "@client/(.*)": "/src/client/$1", 11 | "@server/(.*)": "/src/server/$1" 12 | }, 13 | setupTestFrameworkScriptFile: "/test/config/jest.setup.js" 14 | }; -------------------------------------------------------------------------------- /watson-stt-stream-connector/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | 14 | [*.md] 15 | indent_size = 2 16 | 17 | [*.java] 18 | indent_size = 4 19 | 20 | [*.{html,js,rb,css,xml}] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Suspense} from "react"; 4 | import Dashboard from "@client/components/Dashboard/Dashboard"; 5 | import {AppProvider} from "@client/providers/AppProvider"; 6 | import {Loading} from "@carbon/react"; 7 | 8 | const App = () => { 9 | return ( 10 | }> 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /api-server/public/img/logo-dark.3727fec5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /api-server/public/img/logo-light.73342c25.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/SpeechToTextEngine.js: -------------------------------------------------------------------------------- 1 | 2 | const { Duplex } = require('stream'); 3 | 4 | class SpeechToTextEngine extends Duplex { 5 | /* eslint-disable class-methods-use-this */ 6 | _read() {} 7 | 8 | _write() {} 9 | 10 | /** 11 | * Destroys the Speech To Text Engine if a close from the other side occurs 12 | */ 13 | // eslint-disable-next-line class-methods-use-this 14 | destroy() { 15 | throw new Error('not implemented'); 16 | } 17 | } 18 | module.exports = SpeechToTextEngine; 19 | -------------------------------------------------------------------------------- /wrapper-ui/server/.env-example: -------------------------------------------------------------------------------- 1 | # IBM Cloud Object Storage 2 | COS_ENDPOINT=COS endpoint 3 | COS_API_KEY_ID=COS apikey 4 | COS_SERVICE_INSTANCE_ID=COS service instance ID 5 | COS_SIGNATURE_VERSION=COS signature version 6 | 7 | # IBM AppID 8 | CLIENT_ID=AppID client ID 9 | OAUTH_SERVER_URL=AppID Oauth server URL 10 | PROFILES_URL=AppID profiles URL 11 | APP_ID_SECRET=AppID Secret 12 | TENANT_ID=AppID tenant id 13 | REDIRECT_URI=AppID redirect URL 14 | 15 | # Express 16 | SESSION_SECRET=Express session secret - can be anything -------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/TopBar/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import ActiveSessionBar from "../ActiveSessionBar/ActiveSessionBar"; 2 | import FileStreamer from "../FileStreamer/FileStreamer"; 3 | 4 | const TopBar = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default TopBar; 18 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/index.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | $prefix: 'cds'; 4 | 5 | @use '@carbon/react/index' with ( 6 | $css--default-type: true, 7 | $css--reset: true, 8 | $font-path: '@ibm/plex' 9 | ); 10 | 11 | @use '@carbon/themes/scss/themes'; 12 | @use '@carbon/themes/scss/theme' as *; 13 | 14 | 15 | :root { 16 | color-scheme: light; 17 | @include theme(themes.$g10); 18 | } 19 | 20 | /* 21 | @media screen and (prefers-color-scheme: dark) { 22 | :root { 23 | color-scheme: dark; 24 | @include theme(themes.$g100); 25 | } 26 | } 27 | */ 28 | -------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/RightBar/RightBar.jsx: -------------------------------------------------------------------------------- 1 | import Conversation from "../Conversation/Conversation"; 2 | import SentimentProgress from "../SentimentProgress/SentimentProgress"; 3 | 4 | const RightBar = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default RightBar; 18 | -------------------------------------------------------------------------------- /utilities/mono-to-stereo-wav-converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mono-to-stereo-wav-converter", 3 | "version": "1.0.0", 4 | "description": "Converts a mono wav file of a conversation between 2 people into a stero wav file with a channel for each person.", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Brian Pulito", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "dotenv": "^16.4.5", 13 | "fs": "^0.0.1-security", 14 | "ibm-watson": "^8.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/spanUtils.js: -------------------------------------------------------------------------------- 1 | 2 | const { trace } = require('@opentelemetry/api'); 3 | 4 | const tracer = trace.getTracer('stream-connector'); 5 | // Function to start a new span 6 | function startSpan(operationName, parentSpan) { 7 | if (parentSpan) { 8 | return tracer.withSpan(parentSpan).startSpan(operationName); 9 | } else { 10 | return tracer.startSpan(operationName); 11 | } 12 | } 13 | 14 | // Function to end a span 15 | function endSpan(span) { 16 | span.end(); 17 | } 18 | 19 | module.exports = { 20 | startSpan, 21 | endSpan 22 | }; -------------------------------------------------------------------------------- /api-server/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:16-alpine as base 3 | 4 | 5 | FROM base as builder 6 | 7 | RUN npm config set unsafe-perm true 8 | 9 | WORKDIR /code 10 | 11 | COPY ./ /code 12 | 13 | RUN npm install 14 | 15 | RUN rm -f .npmrc 16 | 17 | FROM node:16-alpine 18 | 19 | 20 | ENV NODE_ENV production 21 | 22 | RUN npm i -g pm2 23 | 24 | # Create app directory 25 | WORKDIR /home/app 26 | 27 | # Copy the built application 28 | COPY --from=builder ["/code", "/home/app"] 29 | 30 | ENV HOME="/home/app" 31 | 32 | EXPOSE 8000 33 | 34 | CMD ["pm2-runtime", "index.js"] 35 | -------------------------------------------------------------------------------- /wrapper-ui/server/utils/websocketRouter.js: -------------------------------------------------------------------------------- 1 | // routes/websocketRouter.js 2 | const express = require("express"); 3 | const { configureWebSocket } = require("../websocket"); 4 | 5 | const websocketRouter = express.Router(); 6 | 7 | module.exports = (wss, handleWebSocketConnection) => { 8 | // WebSocket configuration 9 | wss.on("connection", handleWebSocketConnection); 10 | 11 | // Route for WebSocket client 12 | websocketRouter.get("/websocket-client", (req, res) => { 13 | res.sendFile(__dirname + "/web-socket.html"); 14 | }); 15 | 16 | return websocketRouter; 17 | }; 18 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/server/websockets.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Express} from "express"; 4 | import {createProxyMiddleware} from "http-proxy-middleware"; 5 | 6 | // AAN_SOCKETIO_SERVER has protocol and port inside 7 | const ANN_SOCKETIO_SERVER = process.env.ANN_SOCKETIO_SERVER ? process.env.ANN_SOCKETIO_SERVER : "localhost"; 8 | 9 | export default function setupWebsockets(app: Express) { 10 | app.use( 11 | createProxyMiddleware({ 12 | pathFilter: "/socket.io", 13 | target: `${ANN_SOCKETIO_SERVER}`, 14 | changeOrigin: true, 15 | ws: true 16 | }) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /api-server/middlewares/fileHandler.js: -------------------------------------------------------------------------------- 1 | 2 | const multer = require('multer') 3 | const createError = require('http-errors') 4 | 5 | const upload = multer({ dest: 'uploads/' }) 6 | 7 | module.exports = (fieldName) => (req, res, next) => { 8 | upload.single(fieldName)(req, res, (err) => { 9 | if (err) { 10 | const error = createError(422, 'Invalid Attribute', { 11 | message: `You need to have a single field (${fieldName}) with type as \`file\``, 12 | }) 13 | return res.status(400).json({ 14 | errors: [error], 15 | }) 16 | } 17 | return next() 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /agent-dashboard-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS build 2 | 3 | WORKDIR /agent-dashboard 4 | 5 | COPY src/ src/ 6 | COPY *.json *.js ./ 7 | 8 | RUN npm install --force 9 | RUN npm run build 10 | 11 | FROM node:20 AS runtime 12 | 13 | USER node 14 | 15 | #RUN mkdir -p /agent-dashboard && chown -R node:node /agent-dashboard 16 | 17 | WORKDIR /agent-dashboard 18 | 19 | COPY --from=build /agent-dashboard/dist ./dist 20 | 21 | COPY --from=build /agent-dashboard/node_modules ./node_modules 22 | 23 | COPY --from=build /agent-dashboard/package.json ./ 24 | 25 | EXPOSE 3000 26 | 27 | CMD ["node", "--max-old-space-size=4096", "dist/server/index.js"] -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # -*- mode: Python -* 2 | 3 | 4 | # `Disables Tilt’s snapshots feature, hiding it from the UI. 5 | disable_snapshots() 6 | 7 | # Enable or Disable Telemetry 8 | docker_compose(["./docker-compose.telemetry.yml"]) 9 | services = {'stream-connector': {'environment': {'TELEMETRY': 'true'}}, 'celery-workers': {'environment': {'TELEMETRY': 'true'}}, 'api-server': {'environment': {'TELEMETRY': 'true'}}} 10 | docker_compose(['docker-compose.yml', encode_yaml({'services': services})]) 11 | docker_compose(["./docker-compose.ui.yml"]) 12 | 13 | 14 | # point Tilt at the existing docker-compose configuration. 15 | # docker_compose(["./docker-compose.yml"]) -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/i18n.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import i18n, {InitOptions} from 'i18next'; 4 | import {initReactI18next} from 'react-i18next'; 5 | 6 | import Backend from 'i18next-http-backend'; 7 | import LanguageDetector from 'i18next-browser-languagedetector'; 8 | 9 | const options: InitOptions = { 10 | fallbackLng: "en", 11 | debug: false, 12 | returnNull: false, 13 | interpolation: { 14 | escapeValue: false 15 | }, 16 | backend: { 17 | loadPath: "/locales/{{lng}}.json", 18 | } 19 | }; 20 | 21 | i18n.use(Backend) 22 | .use(LanguageDetector) 23 | .use(initReactI18next) 24 | .init(options); 25 | 26 | export default i18n; -------------------------------------------------------------------------------- /wrapper-ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true, node: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react/jsx-runtime", 8 | "plugin:react-hooks/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs", "server"], 11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 12 | settings: { react: { version: "18.2" } }, 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react-refresh/only-export-components": [ 16 | "warn", 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /wrapper-ui/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-server", 3 | "version": "0.0.1", 4 | "description": "express server for current-ui", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "start-basic": "node app-basic.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "Apache-2.0", 13 | "dependencies": { 14 | "dotenv": "^16.4.5", 15 | "express": "^4.18.2", 16 | "express-session": "^1.18.0", 17 | "http-proxy-middleware": "^3.0.2", 18 | "ibm-cos-sdk": "^1.13.3", 19 | "ibmcloud-appid": "^7.0.0", 20 | "passport": "^0.7.0", 21 | "path": "^0.12.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as React from "react"; 4 | import * as ReactDOM from "react-dom"; 5 | import {createRoot} from "react-dom/client"; 6 | import App from './components/App/App'; 7 | 8 | import "./i18n"; 9 | import "./index.scss"; 10 | 11 | let promise = Promise.resolve(); 12 | 13 | if (process.env.NODE_ENV !== "production") { 14 | const axe = require("@axe-core/react"); 15 | promise.then(axe.default(React, ReactDOM, 1000)); 16 | } 17 | 18 | promise.finally(() => { 19 | let rootElem = document.getElementById("root") as HTMLElement; 20 | const root = createRoot(rootElem); 21 | root.render(); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /api-server/public/css/app.6e3b9661.css: -------------------------------------------------------------------------------- 1 | .chart[data-v-0ad5cc14],.chart[data-v-68c0c5d5]{max-width:160px;margin:20px}.selector[data-v-2c330798]{max-width:200px}.row-pointer[data-v-1d29c60a] tbody>tr:hover{cursor:pointer}.select-room[data-v-5631eb89]{max-width:200px}.row-pointer[data-v-5631eb89] tbody>tr:hover{cursor:pointer}.key-column[data-v-3c0dcfcd]{width:30%}.link[data-v-3c0dcfcd]{color:inherit}.key-column[data-v-18284f59]{width:30%}.row-pointer[data-v-57b53591] tbody>tr:hover,.row-pointer[data-v-29992f63] tbody>tr:hover{cursor:pointer}.key-column[data-v-8d2424e4]{width:30%}.row-pointer[data-v-38772079] tbody>tr:hover,.row-pointer[data-v-c9425064] tbody>tr:hover{cursor:pointer}.link[data-v-2c2337d4]{color:inherit} -------------------------------------------------------------------------------- /agent-dashboard-ui/src/server/telemetry.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {NodeSDK} from "@opentelemetry/sdk-node"; 4 | import {ConsoleSpanExporter} from "@opentelemetry/sdk-trace-node"; 5 | import {ConsoleMetricExporter, PeriodicExportingMetricReader} from "@opentelemetry/sdk-metrics"; 6 | import {getNodeAutoInstrumentations} from "@opentelemetry/auto-instrumentations-node"; 7 | 8 | export default function setupTelemetry() { 9 | const sdk = new NodeSDK({ 10 | traceExporter: new ConsoleSpanExporter(), 11 | metricReader: new PeriodicExportingMetricReader({ 12 | exporter: new ConsoleMetricExporter(), 13 | }), 14 | instrumentations: [getNodeAutoInstrumentations()], 15 | }); 16 | 17 | sdk.start(); 18 | } 19 | -------------------------------------------------------------------------------- /wrapper-ui/.env-example: -------------------------------------------------------------------------------- 1 | # Server env variables for local development 2 | OTEL_SERVICE_NAME=agent-wrapper-ui 3 | PORT=3003 4 | ANN_AGENT_DASHBOARD=ANN_AGENT_DASHBOARD-http://localhost:3000 5 | ANN_SOCKETIO_SERVER=ANN_SOCKETIO_SERVER-http://localhost:8000 6 | 7 | # All envs for the frontend need to be named specifically for Vite 8 | # Not sure if this should be hard coded in 9 | VITE_MQTT_CALLER_ID=MQTT caller ID - might be able to get with AppID login instead 10 | 11 | # VITE_LOCAL_MICROSERVICES=true #uncomment if running locally (uses /ws instead of /wss) 12 | # Watsonx Assistant - currently unused 13 | # VITE_WA_INTEGRATION_ID=WA integration ID 14 | # VITE_WA_REGION=WA region 15 | # VITE_WA_SERVICE_INSTANCE_ID=WA service instance ID -------------------------------------------------------------------------------- /wrapper-ui/src/utils/data.js: -------------------------------------------------------------------------------- 1 | import angerIcon from "/anger.png"; 2 | import disgustIcon from "/disgusting.png"; 3 | import joyIcon from "/joy.png"; 4 | import fearIcon from "/fear.png"; 5 | import neutralIcon from "/neutral.png"; 6 | import sadnessIcon from "/sadness.png"; 7 | 8 | export const sentimentIcons = { 9 | anger: angerIcon, 10 | disgust: disgustIcon, 11 | joy: joyIcon, 12 | fear: fearIcon, 13 | neutral: neutralIcon, 14 | sadness: sadnessIcon, 15 | }; 16 | export const sessionUsers = []; 17 | 18 | export const CallSummary = []; 19 | 20 | export const conversation = []; 21 | 22 | export const bestActionData = []; 23 | 24 | export const bestActionDataDummy = []; 25 | 26 | export const sentimentData = []; 27 | -------------------------------------------------------------------------------- /wrapper-ui/landing/images/app_id_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /agent-dashboard-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUncheckedIndexedAccess": true, 14 | "module": "NodeNext", 15 | "lib": [ 16 | "es2022", 17 | "dom", 18 | "dom.iterable" 19 | ], 20 | "sourceMap": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": ".", 23 | "paths": { 24 | "@client/*": [ 25 | "src/client/*" 26 | ], 27 | "@server/*": [ 28 | "src/server/*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "./src/**/*", 34 | "./src/@types/*.ts" 35 | ] 36 | } -------------------------------------------------------------------------------- /wrapper-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as builder 2 | 3 | WORKDIR /home/node/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm ci 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | FROM node:18-alpine 14 | 15 | USER node 16 | 17 | RUN mkdir -p /home/node/app && chown -R node:node /home/node/app 18 | 19 | RUN mkdir -p /home/node/app/server/node_modules && chown -R node:node /home/node/app/server/node_modules 20 | 21 | WORKDIR /home/node/app 22 | 23 | COPY --from=builder /home/node/app/dist ./dist 24 | 25 | COPY --from=builder /home/node/app/landing ./landing 26 | 27 | COPY --from=builder /home/node/app/server ./server 28 | 29 | WORKDIR /home/node/app/server 30 | 31 | RUN npm ci --omit=dev 32 | 33 | EXPOSE 3003 34 | 35 | # Express server with AppID 36 | #CMD [ "npm", "start" ] 37 | 38 | # Express server without AppID 39 | CMD [ "npm", "run", "start-basic" ] 40 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/NextBestActions/BestAction.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | @use '@carbon/themes'; 4 | 5 | .actionTile { 6 | border-width: 1px; 7 | border-style: solid; 8 | margin-bottom: 8px; 9 | 10 | a { 11 | background-color: themes.$layer-02; 12 | } 13 | } 14 | 15 | .tooltip { 16 | width: 100%; 17 | } 18 | 19 | .active { 20 | border-color: themes.$support-success; 21 | 22 | * { 23 | fill: themes.$support-success;; 24 | } 25 | } 26 | 27 | .stale { 28 | border-color: themes.$support-warning; 29 | 30 | * { 31 | fill: themes.$support-warning;; 32 | } 33 | } 34 | 35 | .expired { 36 | border-color: themes.$support-error; 37 | 38 | * { 39 | fill: themes.$support-error;; 40 | } 41 | } 42 | 43 | .complete { 44 | border-color: themes.$support-info; 45 | 46 | * { 47 | fill: themes.$support-info;; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /wrapper-ui/src/pages/Dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import TopBar from "../../components/Dashboard/TopBar/TopBar"; 2 | import MiddleBox from "../../components/Dashboard/MiddleBox/MiddleBox"; 3 | import RightBar from "../../components/Dashboard/RightBar/RightBar"; 4 | import SessionBar from "../../components/Dashboard/SessionBar/SessionBar"; 5 | 6 | const Dashboard = () => { 7 | return ( 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Dashboard; 28 | -------------------------------------------------------------------------------- /wrapper-ui/src/main.jsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | // import React from "react"; 3 | import App from "./App.jsx"; 4 | import ReactDOM from "react-dom/client"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { AppContextProvider } from "./context/context.jsx"; 7 | import { ChakraProvider } from "@chakra-ui/react"; 8 | import { AgentIdProvider } from "./hooks/useAgentIdProvider.jsx"; 9 | import { IoProvider } from 'socket.io-react-hook'; // for socketio connectivity 10 | 11 | ReactDOM.createRoot(document.getElementById("root")).render( 12 | // 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | // 25 | ); 26 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import ExtractedEntities from "@client/components/ExtractedEntities/ExtractedEntities"; 5 | import {Column, Grid} from "@carbon/react"; 6 | import NextBestActions from "@client/components/NextBestActions/NextBestActions"; 7 | import CallSummary from "@client/components/CallSummary/CallSummary"; 8 | import WatsonxAssistant from "@client/components/WatsonxAssistant/WatsonxAssistant"; 9 | 10 | import * as styles from "./Dashboard.module.scss"; 11 | 12 | const Dashboard = () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default Dashboard; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-assist", 3 | "version": "1.0.0", 4 | "description": "Extract data from PDF files using watsonx prompts", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "license:add": "npx license-check-and-add add -f license-check-and-add.config.json", 9 | "license:check": "npx license-check-and-add check -f license-check-and-add.config.json", 10 | "license:remove": "npx license-check-and-add remove -f license-check-and-add.config.json" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ibm/agent-assist.git" 15 | }, 16 | "keywords": [ 17 | "watsonx", 18 | "react-admin", 19 | "ui" 20 | ], 21 | "author": "Bob Fang", 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/ibm/agent-assist/issues" 25 | }, 26 | "homepage": "https://github.com/ibm/agent-assist#readme" 27 | } 28 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/providers/EnvVars.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {createContext, useContext, useEffect, useState} from "react"; 4 | import {WidgetConfigT} from "../../@types/widget"; 5 | 6 | export interface EnvVars { 7 | widgetConfig: WidgetConfigT, 8 | socketPort: number, 9 | waIntegrationId: string, 10 | waRegion: string, 11 | waServiceInstanceId: string 12 | } 13 | 14 | const EnvVarsContext = createContext({}); 15 | 16 | export const EnvVarsProvider = (props: any) => { 17 | const [envVars, setEnvVars] = useState({}); 18 | 19 | useEffect(() => { 20 | fetch('api/environment') 21 | .then((response) => response.json()) 22 | .then((data) => { 23 | setEnvVars(data); 24 | }).catch(err => err) 25 | }, []) 26 | 27 | return {props.children}; 28 | } 29 | 30 | export const useEnvVars = () => useContext(EnvVarsContext); 31 | -------------------------------------------------------------------------------- /wrapper-ui/landing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample App 5 | 6 | 7 | 8 | 9 |
10 | 17 |
Welcome to
IBM App ID
18 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /web-stream-client/simple_websocket_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import json 4 | 5 | async def audio_stream(websocket, path): 6 | print("WebSocket connection established.") 7 | 8 | total_bytes_received = 0 9 | while True: 10 | try: 11 | data = await websocket.recv() 12 | 13 | if (total_bytes_received == 0): 14 | # Send the 'opened' back to kick off the file send 15 | await websocket.send('{"type":"opened"}') 16 | 17 | total_bytes_received += len(data) 18 | print(f"Total bytes received: {total_bytes_received}") 19 | 20 | # client disconnected? 21 | except websockets.ConnectionClosedOK: 22 | print(f"Web socket closed") 23 | break 24 | 25 | async def main(): 26 | server = await websockets.serve(audio_stream, "localhost", 8080) 27 | 28 | print("WebSocket server started at ws://localhost:8080") 29 | await server.wait_closed() 30 | 31 | asyncio.run(main()) -------------------------------------------------------------------------------- /wrapper-ui/src/hooks/useAuthenticatedSocket.js: -------------------------------------------------------------------------------- 1 | import { useSocket } from "socket.io-react-hook"; 2 | //import { useOidcConfig } from 'utils/oidcProvider' 3 | export const useAuthenticatedSocket = (namespace) => { 4 | //const { appIDConfigured, accessToken } = useOidcConfig(); 5 | // Manipulate enabled option based on OIDC configuration and access token 6 | // console.log(`socketio - oidc ${appIDConfigured} at ${accessToken}`) 7 | // console.log(getAccessToken()) 8 | // expose the useSocket returns here 9 | // we wait for appIDConfigured to turn from null to either true/false before enabling the connection 10 | // if appIDConfigured is true, we continue waiting until the accessToken becomes available (it is handled in the authProvider) 11 | const { socket, connected, error } = useSocket(namespace, { 12 | // enabled: appIDConfigured === true // && !!accessToken, 13 | // auth: appIDConfigured === true ? { token: accessToken } : undefined 14 | }); 15 | 16 | return { socket, connected, error }; 17 | }; -------------------------------------------------------------------------------- /api-server/public/index.html: -------------------------------------------------------------------------------- 1 | Socket.IO Admin UI
-------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/MiddleBox/MiddleBox.jsx: -------------------------------------------------------------------------------- 1 | import {useAgentId} from "../../../hooks/useAgentIdProvider.jsx"; 2 | import {Loading} from "@carbon/react" 3 | 4 | const MiddleBox = () => { 5 | const {agentId} = useAgentId(); 6 | if (agentId) { 7 | if (window.__APP_CONFIG__) { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 | ); 15 | } else { 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | 26 | } else { 27 | return () 28 | } 29 | }; 30 | 31 | export default MiddleBox; 32 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | 3 | # pull official base image 4 | FROM node:16-alpine 5 | 6 | # set working directory 7 | WORKDIR /app 8 | 9 | # add `/app/node_modules/.bin` to $PATH 10 | ENV PATH /app/node_modules/.bin:$PATH 11 | 12 | # install app dependencies 13 | COPY package.json ./ 14 | COPY package-lock.json ./ 15 | 16 | RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* 17 | RUN npm install 18 | 19 | 20 | # switch user 21 | USER node 22 | 23 | # add app 24 | COPY . ./ 25 | 26 | # Expose the port 27 | EXPOSE 8080 28 | 29 | # Environment variables (note that these should be moved to an external config map) 30 | ENV MQTT_BROKER_URL= 31 | ENV MQTT_BROKER_USER_NAME=apikey 32 | ENV MQTT_BROKER_PASSWORD= 33 | ENV MQTT_BROKER_PATH=/ws 34 | ENV MQTT_BROKER_PORT=443 35 | ENV MQTT_BROKER_DISABLE_SLL=false 36 | 37 | ENV WATSON_STT_USERNAME=apikey 38 | ENV WATSON_STT_PASSWORD= 39 | ENV WATSON_STT_URL= 40 | ENV WATSON_STT_MODEL=en-US_Telephony 41 | ENV WATSON_STT_END_OF_PHRASE_SILENCE_TIME=1.3 42 | 43 | ENV DEFAULT_SERVER_LISTEN_PORT=8080 44 | ENV STREAM_CONNECTOR_API_KEY= 45 | ENV STREAM_ADAPTER_TYPE=GenesysAudioHookAdapter 46 | ENV LOG_LEVEL=debug 47 | 48 | # start app 49 | CMD ["npm", "start"] 50 | -------------------------------------------------------------------------------- /docker-compose.ui.yml: -------------------------------------------------------------------------------- 1 | 2 | # docker-compose.yml 3 | 4 | ## TODO: substitute localhost references in env with correct service names 5 | 6 | version: '3.7' 7 | services: 8 | # this is the component that is embedded by an iframe in CCAS 9 | agent-dashboard: 10 | build: ./agent-dashboard-ui 11 | container_name: aan-agent-dashboard 12 | ports: 13 | - '3000:3000' 14 | restart: 'unless-stopped' 15 | environment: 16 | - OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME-agent-dashboard-ui} 17 | - PORT=3000 18 | - ANN_SOCKETIO_SERVER=${ANN_SOCKETIO_SERVER-http://api-server:8000} 19 | - ANN_WRAPPER_DASHBOARD=${ANN_WRAPPER_DASHBOARD-http://localhost:3003} 20 | - WA_INTEGRATION_ID=${WA_INTEGRATION_ID} 21 | - WA_REGION=${WA_REGION} 22 | - WA_SERVICE_INSTANCE_ID=${WA_SERVICE_INSTANCE_ID} 23 | 24 | wrapper-ui: 25 | build: ./wrapper-ui 26 | container_name: aan-wrapper-ui 27 | ports: 28 | - '3003:3003' 29 | restart: 'unless-stopped' 30 | environment: 31 | - OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME-agent-wrapper-ui} 32 | - PORT=3003 33 | - ANN_AGENT_DASHBOARD=${ANN_AGENT_DASHBOARD-http://localhost:3000} 34 | - ANN_SOCKETIO_SERVER=${ANN_SOCKETIO_SERVER-http://api-server:8000} 35 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/StreamingSessionState.js: -------------------------------------------------------------------------------- 1 | 2 | class StreamingSessionState { 3 | 4 | sessionId; 5 | 6 | // Used to track when the speech engines are ready to receive audio. 7 | internalSpeechEngineListening = false; 8 | externalSpeechEngineListening = false; 9 | 10 | // Used to cache any audio that is received from the CCaaS prior to the speech engine 11 | // being ready to receive it. 12 | preListenCache = {}; 13 | 14 | constructor(sessionId) { 15 | this.sessionId = sessionId; 16 | } 17 | 18 | getPreListenCache(channelName){ 19 | return this.preListenCache[channelName]; 20 | } 21 | 22 | setPreListenCache(channelName, value){ 23 | this.preListenCache[channelName] = value; 24 | } 25 | 26 | isSpeechEngineListening(channelName){ 27 | if (channelName == 'internal') 28 | return(this.internalSpeechEngineListening); 29 | else 30 | return(this.externalSpeechEngineListening); 31 | } 32 | 33 | setSpeechEngineListening(channelName, value){ 34 | if (channelName == 'internal') 35 | this.internalSpeechEngineListening = value; 36 | else 37 | this.externalSpeechEngineListening = value; 38 | } 39 | } 40 | module.exports = StreamingSessionState; 41 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | 62 | # tern file 63 | .tern-project 64 | -------------------------------------------------------------------------------- /celery/BaseAgent/ConfigCache.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import threading 4 | 5 | class ConfigCache: 6 | def __init__(self, endpoint, polling_interval=60): 7 | self.endpoint = endpoint 8 | self.config = None 9 | self.polling_interval = polling_interval 10 | self.last_updated = 0 11 | self._start_polling() 12 | 13 | def _fetch_config(self): 14 | try: 15 | response = requests.get(self.endpoint) 16 | if response.status_code == 200: 17 | return response.json() 18 | else: 19 | print(f"Failed to fetch configuration. Status code: {response.status_code}") 20 | except Exception as e: 21 | print(f"Error fetching configuration: {e}") 22 | return None 23 | 24 | def _update_config(self): 25 | while True: 26 | config = self._fetch_config() 27 | if config: 28 | self.config = config 29 | self.last_updated = time.time() 30 | time.sleep(self.polling_interval) 31 | 32 | def _start_polling(self): 33 | polling_thread = threading.Thread(target=self._update_config, daemon=True) 34 | polling_thread.start() 35 | 36 | def get_config(self): 37 | return self.config 38 | -------------------------------------------------------------------------------- /docker-compose.telemetry.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | # jaeger-collector: 5 | # image: jaegertracing/jaeger-collector:latest 6 | # ports: 7 | # - "14250:14250" 8 | # - "14268:14268" 9 | # - "14269:14269" 10 | # environment: 11 | # - SPAN_STORAGE_TYPE=memory 12 | 13 | # jaeger-query: 14 | # image: jaegertracing/jaeger-query:latest 15 | # ports: 16 | # - "16686:16686" 17 | # depends_on: 18 | # - jaeger-collector 19 | 20 | # jaeger-agent: 21 | # image: jaegertracing/jaeger-agent:latest 22 | # ports: 23 | # - "5775:5775/udp" 24 | # - "6831:6831/udp" 25 | # - "6832:6832/udp" 26 | # - "5778:5778" 27 | # environment: 28 | # - COLLECTOR_HOST_PORT=jaeger-collector:14267 29 | # - REPORTER_LOG_SPANS=true 30 | jaeger: 31 | image: jaegertracing/all-in-one:1.55 32 | container_name: jaeger 33 | environment: 34 | - COLLECTOR_OTLP_ENABLED=true 35 | - COLLECTOR_ZIPKIN_HOST_PORT=:9411 36 | ports: 37 | - "5775:5775/udp" 38 | - "6831:6831/udp" 39 | - "6832:6832/udp" 40 | - "5778:5778" 41 | - "16686:16686" 42 | - "14250:14250" 43 | - "14268:14268" 44 | - "14269:14269" 45 | - "4317:4317" 46 | - "4318:4318" 47 | - "9411:9411" -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/providers/Socket.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {v4 as uuid} from "uuid"; 4 | import {useSocket as useSocketParent, useSocketEvent as useSocketEventParent} from "socket.io-react-hook"; 5 | import {createContext, useContext} from "react"; 6 | 7 | const queryParams = new URLSearchParams(window.location.search); 8 | const agentId: any = queryParams.get('agent_id') || uuid(); 9 | 10 | const SocketContext = createContext({ 11 | socket: undefined, 12 | setSocket: (socket: any) => { 13 | } 14 | }); 15 | 16 | export const useSocket = () => { 17 | let {socket, setSocket} = useContext(SocketContext); 18 | 19 | if (socket === undefined) { 20 | const {socket} = useSocketParent(); 21 | socket.on('connect', () => { 22 | console.log(agentId); 23 | socket.emit("joinRoom", agentId) 24 | }); 25 | setSocket(socket); 26 | } 27 | 28 | return useSocketParent(socket); 29 | }; 30 | 31 | export const useSocketEvent = (eventName: string) => { 32 | const {socket} = useSocket(); 33 | return useSocketEventParent(socket, eventName); 34 | } 35 | 36 | export type SocketParameters = { 37 | title: string; 38 | value: string; 39 | text: string; 40 | action_id: number; 41 | session_id: string; 42 | }; 43 | 44 | export type SocketPayload = { 45 | type: string; 46 | parameters: SocketParameters; 47 | }; -------------------------------------------------------------------------------- /watson-stt-stream-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming.stt.adapter", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon ./app.js | pino", 8 | "start": "node --require '@opentelemetry/auto-instrumentations-node/register' ./app.js | pino", 9 | "lint": "eslint .", 10 | "test": "eslint . && LOG_LEVEL=error mocha test/" 11 | }, 12 | "author": "", 13 | "license": "SEE LICENSE IN LICENSE", 14 | "dependencies": { 15 | "@opentelemetry/api": "^1.8.0", 16 | "@opentelemetry/auto-instrumentations-node": "^0.44.0", 17 | "@opentelemetry/exporter-collector-proto": "^0.25.0", 18 | "@opentelemetry/exporter-jaeger": "^1.23.0", 19 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.50.0", 20 | "@opentelemetry/sdk-node": "^0.50.0", 21 | "camelcase": "^5.3.1", 22 | "celery-node": "^0.5.9", 23 | "config": "^1.29.2", 24 | "dotenv": "^16.4.5", 25 | "ibm-watson": "^5.4.0", 26 | "mqtt": "^5.3.3", 27 | "pino": "^4.10.2", 28 | "ws": "^3.3.3" 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.1.2", 32 | "eslint": "^4.13.1", 33 | "eslint-config-airbnb-base": "^12.1.0", 34 | "eslint-plugin-import": "^2.8.0", 35 | "mocha": "^4.0.1", 36 | "nodemon": "^1.14.0", 37 | "tern": "^0.21.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /wrapper-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "help_dashboard", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build --base=./", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "prettier": "prettier --write ." 12 | }, 13 | "dependencies": { 14 | "@carbon/charts-react": "^1.15.4", 15 | "@carbon/react": "^1.53.1", 16 | "@chakra-ui/react": "^2.8.2", 17 | "date-fns": "^2.27.0", 18 | "lodash": "^4.17.21", 19 | "paho-mqtt": "^1.1.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-gauge-chart": "^0.4.1", 23 | "react-icons": "^5.0.1", 24 | "react-router-dom": "^6.21.2", 25 | "socket.io-react-hook": "^2.4.4", 26 | "uuid": "^9.0.1" 27 | }, 28 | "devDependencies": { 29 | "@types/react": "^18.2.15", 30 | "@types/react-dom": "^18.2.7", 31 | "@vitejs/plugin-react": "^4.0.3", 32 | "autoprefixer": "^10.4.16", 33 | "eslint": "^8.45.0", 34 | "eslint-plugin-react": "^7.32.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.3", 37 | "postcss": "^8.4.33", 38 | "prettier": "^3.2.5", 39 | "tailwindcss": "^3.4.1", 40 | "vite": "^4.4.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/client/components/CallSummary/CallSummary.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as styles from "./CallSummary.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} from "@client/providers/Socket"; 8 | import {InlineLoading} from "@carbon/react"; 9 | 10 | const CallSummary = () => { 11 | const [summary, setSummary] = useState(""); 12 | const {t} = useTranslation(); 13 | const {lastMessage} = useSocketEvent('celeryMessage') 14 | 15 | useEffect(() => { 16 | if (lastMessage) { 17 | const payload: SocketPayload = JSON.parse(lastMessage?.payloadString); 18 | 19 | if (payload?.type === "summary" && payload?.parameters?.text) { 20 | setSummary(payload?.parameters?.text?.trim() || ""); 21 | } 22 | } 23 | }, [lastMessage]) 24 | 25 | return ( 26 |
27 |
28 | {t("callSummary")} 29 |
30 |
31 |
32 |           {summary ? summary : }
33 |         
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default CallSummary; -------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/ActiveSessionBar/ActiveSessionBar.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { IoSettingsOutline } from "react-icons/io5"; 3 | import { AppContext } from "../../../context/context"; 4 | import { SettingModal } from "../../Modal/Setting/SettingModal"; 5 | 6 | const ActiveSessionBar = () => { 7 | const { currentSessionUser } = useContext(AppContext); 8 | 9 | return ( 10 |
11 | {currentSessionUser ? ( 12 | <> 13 |
18 | 19 |
20 | 21 |

28 | Observing call with {currentSessionUser?.phone} 29 |

30 | 31 | ) : ( 32 |

No session selected

33 | )} 34 |
35 | ); 36 | }; 37 | 38 | 39 | 40 | export default ActiveSessionBar; 41 | -------------------------------------------------------------------------------- /license-check-and-add.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | ".idea", 4 | ".minio_storage", 5 | ".git", 6 | ".gitkeep", 7 | ".github", 8 | ".pgdata", 9 | ".tmp", 10 | ".vscode", 11 | "docs", 12 | "license", 13 | "stylesheets", 14 | ".editorconfig", 15 | ".prettierignore", 16 | ".prettierrc", 17 | "**/*.pyc", 18 | "**/*.gz", 19 | "**/*.mp3", 20 | "**/sqitch.plan", 21 | "**/build", 22 | "**/static", 23 | "**/tmp", 24 | "detpyvenv", 25 | ".DS_Store" 26 | ], 27 | "license": "license", 28 | "defaultFormat": { 29 | "eachLine": { 30 | "prepend": "# " 31 | } 32 | }, 33 | "licenseFormats": { 34 | "gitignore|npmignore|eslintignore|dockerignore|sh|py|editorconfig|txt|yml|yaml": { 35 | "eachLine": { 36 | "prepend": "# " 37 | } 38 | }, 39 | "html|xml|svg|md": { 40 | "prepend": "" 42 | }, 43 | "js|ts|css|scss|less|php|as|c|java|cpp|go|cto|acl": { 44 | "prepend": "/*", 45 | "append": " */", 46 | "eachLine": { 47 | "prepend": " * " 48 | } 49 | }, 50 | "sql": { 51 | "eachLine": { 52 | "prepend": "-- " 53 | } 54 | }, 55 | "dotfile|env|dev|local|k8|example|env_example": { 56 | "eachLine": { 57 | "prepend": "# " 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /celery/swarmLauncher.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import subprocess 4 | import celery 5 | 6 | # Define a function to generate a random string of characters 7 | def generate_random_string(length): 8 | letters = string.ascii_lowercase 9 | return ''.join(random.choice(letters) for _ in range(length)) 10 | 11 | # Prefix for the worker name 12 | worker_name_prefix = "agent-" 13 | 14 | # Generate a random suffix for the worker name 15 | random_suffix = generate_random_string(8) 16 | 17 | # Concatenate the prefix and random suffix to create the worker name 18 | worker_name = worker_name_prefix + random_suffix 19 | 20 | # Start the Celery worker with the generated hostname 21 | subprocess.Popen(f"celery -A celery_worker worker --loglevel=INFO --hostname={worker_name_prefix + generate_random_string(8)}", shell=True) 22 | subprocess.Popen(f"celery -A celery_worker worker --loglevel=INFO --hostname={worker_name_prefix + generate_random_string(8)}", shell=True) 23 | subprocess.Popen(f"celery -A celery_worker worker --loglevel=INFO --hostname={worker_name_prefix + generate_random_string(8)}", shell=True) 24 | # subprocess.Popen(f"celery -A celery_worker worker --loglevel=INFO --hostname={worker_name_prefix + generate_random_string(8)}", shell=True) 25 | # subprocess.Popen(f"celery -A celery_worker worker --loglevel=INFO --hostname={worker_name_prefix + generate_random_string(8)}", shell=True) 26 | -------------------------------------------------------------------------------- /agent-dashboard-ui/src/server/routes/environment.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Router} from "express"; 4 | import {isLeft} from "fp-ts/Either"; 5 | import {PathReporter} from "io-ts/PathReporter"; 6 | import {WidgetConfig, WidgetConfigT} from "../../@types/widget"; 7 | 8 | const WIDGET_CONFIG = JSON.parse(process.env.WIDGET_CONFIG ? 9 | process.env.WIDGET_CONFIG : 10 | '{"widgets": [{"type": "ExtractedEntities", "minColumns": 1}]}'); 11 | 12 | const SOCKET_PORT = process.env.SOCKET_PORT ? parseInt(process.env.SOCKET_PORT) : 8000; 13 | 14 | const envRouter = Router(); 15 | 16 | const decoded = WidgetConfig.decode(WIDGET_CONFIG); 17 | 18 | if (isLeft(decoded)) { 19 | throw Error( 20 | `Invalid widget config: ${PathReporter.report(decoded).join("\n")}` 21 | ); 22 | } 23 | 24 | const decodedWidgetConfig: WidgetConfigT = decoded.right; // now safely the correct type 25 | 26 | 27 | export default function setupEnvironment(router: Router) { 28 | envRouter.get("/", (req, res) => { 29 | res.status(200).json({ 30 | widgetConfig: decodedWidgetConfig, 31 | socketPort: SOCKET_PORT, 32 | waIntegrationId: process.env.WA_INTEGRATION_ID, 33 | waRegion: process.env.WA_REGION, 34 | waServiceInstanceId: process.env.WA_SERVICE_INSTANCE_ID 35 | }); 36 | }); 37 | 38 | router.use('/environment', envRouter); 39 | return router; 40 | } 41 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/.env-example: -------------------------------------------------------------------------------- 1 | # RabbitMQ 2 | MQTT_BROKER_USER_NAME=MQTT broker username - default `apikey` 3 | MQTT_BROKER_PASSWORD=MQTT broker password 4 | MQTT_BROKER_URL=MQTT broker host URL - typically this is a websocket (wss://) based URL 5 | MQTT_BROKER_PORT=MQTT broker port - default 443 6 | MQTT_BROKER_PATH=MQTT broker path - default `/ws` 7 | 8 | # Watson STT 9 | WATSON_STT_URL=Watson STT URL 10 | WATSON_STT_USERNAME=Watson STT username - default `apikey` 11 | WATSON_STT_PASSWORD=Watson STT password 12 | WATSON_STT_MODEL=Watson STT model - default `en-US_Telephony_LSM` 13 | WATSON_STT_END_OF_PHRASE_SILENCE_TIME=Watson STT Specifies the duration in seconds as a a floating point number of the audio pause interval at which the service splits a transcript into multiple events - default `2.0` 14 | STREAM_CONNECTOR_API_KEY=This is the API KEY use to authenticate all inbound connections. The service expects that this api key will be passed in the HTTP request that opens the websocket in the following header: 'x-api-key'. It is up to the person deploying this service to generate a UUID for this value 15 | STREAM_ADAPTER_TYPE=This service is design to support multiple streaming APIs - currently, the only supported adapters are: 'GenesysAudioHookAdapter' (which is the default) and the 'WebStreamingAdapter' 16 | LOG_LEVEL=Default is `debug`- choices include `fatal`, `error`, `warn`, `info`, `debug`, `trace`, or `silent` -------------------------------------------------------------------------------- /api-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-assist/api-server", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "develop": "nodemon server", 7 | "start": "cross-env NODE_ENV=production node index" 8 | }, 9 | "dependencies": { 10 | "@socket.io/admin-ui": "^0.5.1", 11 | "@socket.io/postgres-adapter": "^0.3.1", 12 | "@socket.io/postgres-emitter": "^0.1.0", 13 | "body-parser": "^1.19.0", 14 | "celery-node": "^0.5.9", 15 | "compression": "^1.7.4", 16 | "cookie-parser": "^1.4.6", 17 | "cors": "^2.8.5", 18 | "cross-env": "^7.0.3", 19 | "csvtojson": "^2.0.10", 20 | "dataframe-js": "^1.4.4", 21 | "dotenv": "^8.6.0", 22 | "dotenv-expand": "^5.1.0", 23 | "express": "^4.18.1", 24 | "express-jwt": "^8.4.1", 25 | "express-session": "^1.18.0", 26 | "hashmap": "^2.4.0", 27 | "hpp": "^0.2.3", 28 | "http-errors": "^1.8.1", 29 | "http-proxy-middleware": "^2.0.6", 30 | "ibm-cos-sdk": "^1.12.0", 31 | "jsonwebtoken": "^9.0.2", 32 | "jwks-rsa": "^3.1.0", 33 | "morgan": "^1.10.0", 34 | "multer": "^1.4.5-lts.1", 35 | "nconf": "^0.12.0", 36 | "passport": "^0.7.0", 37 | "passport-jwt": "^4.0.1", 38 | "pg": "^8.11.3", 39 | "socket.io": "^4.7.4", 40 | "superagent": "^6.1.0", 41 | "swagger-ui-express": "^4.4.0" 42 | }, 43 | "devDependencies": { 44 | "nodemon": "^3.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | # Change these 2 | WATSON_STT_USERNAME=apikey 3 | WATSON_STT_PASSWORD=**** 4 | WATSON_STT_URL=https://api.us-east.speech-to-text.watson.cloud.ibm.com/instances/**** 5 | AAN_WML_APIKEY= 6 | AAN_WML_URL=https://us-south.ml.cloud.ibm.com 7 | AAN_WML_PROJECT_ID= 8 | 9 | AAN_NLU_URL=https://api.us-south.natural-language-understanding.watson.cloud.ibm.com/instances/**** 10 | AAN_NLU_API_KEY= 11 | 12 | AAN_ASSISTANT_APIKEY= 13 | AAN_ASSISTANT_URL=https://api.us-south.assistant.watson.cloud.ibm.com/instances/*** 14 | AAN_ASSISTANT_ENVIRONMENT_ID=environment_id 15 | 16 | # These are the variables related to the Assistant you are connecting to for the dashboard webchat widget 17 | WA_INTEGRATION_ID= 18 | WA_REGION= 19 | WA_SERVICE_INSTANCE_ID= 20 | 21 | # Optionally change these 22 | WATSON_STT_MODEL=en-US_Telephony_LSM 23 | WATSON_STT_END_OF_PHRASE_SILENCE_TIME=1.3 24 | AAN_ENTITY_EXTRACTION_LLM_MODEL_NAME=meta-llama/llama-3-70b-instruct 25 | AAN_ENTITY_EXTRACTION_ENTITIES=First Name, Last Name, Email Address, Product, Issue, Street Address, City, State, ZIP Code 26 | AAN_SUMMARIZATION_LLM_MODEL_NAME=meta-llama/llama-3-70b-instruct 27 | 28 | # Default for local compose do not change 29 | STREAM_ADAPTER_TYPE=GenesysAudioHookAdapter 30 | STREAM_CONNECTOR_API_KEY= 31 | LOG_LEVEL=trace 32 | NODE_ENV=dev 33 | DEFAULT_SERVER_LISTEN_PORT=8080 34 | AAN_REDIS_URI=redis://redis:6379/1 35 | AAN_AMQP_URI=amqp://admin:adminpass@rabbitmq:5672 -------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/Conversation/Message/Message.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useEffect, useState } from "react"; 3 | 4 | const Message = ({ data }) => { 5 | const [isAgentMessage, setIsAgentMessage] = useState(null); 6 | 7 | useEffect(() => { 8 | if (!data) return; 9 | setIsAgentMessage(data?.user_type === "agent"); 10 | }, [data]); 11 | 12 | if (isAgentMessage === null) { 13 | return <>; 14 | } 15 | return ( 16 |
17 |
18 |

21 | {isAgentMessage ? "Agent" : "Customer"} 22 |

23 |
30 |

33 | {data.text} 34 |

35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Message; 42 | 43 | Message.propTypes = { 44 | data: PropTypes.any, 45 | }; 46 | -------------------------------------------------------------------------------- /wrapper-ui/server/utils/authRouter.js: -------------------------------------------------------------------------------- 1 | // routes/authRouter.js 2 | const express = require("express"); 3 | const passport = require("passport"); 4 | const WebAppStrategy = require("ibmcloud-appid").WebAppStrategy; 5 | const path = require("path"); 6 | 7 | const authRouter = express.Router(); 8 | 9 | // Configure App ID authentication 10 | passport.use( 11 | new WebAppStrategy({ 12 | tenantId: "f9afd1d8-eaec-46b6-aa75-206f066f5711", // Replace with your App ID tenant ID 13 | clientId: "2a00a6b8-aae8-479b-b111-d6afe59b9379", // Replace with your App ID client ID 14 | secret: "ZTVjNTZkZWItNmVkOC00NmE2LTg5M2UtNDAyNzQ2ZTkxMjk2", // Replace with your App ID secret 15 | oauthServerUrl: 16 | "https://us-south.appid.cloud.ibm.com/oauth/v4/f9afd1d8-eaec-46b6-aa75-206f066f5711", // Replace with your App ID region 17 | redirectUri: "http://localhost:3000/ibm/cloud/appid/callback", 18 | }), 19 | ); 20 | 21 | // Serialize and deserialize user 22 | passport.serializeUser((user, cb) => cb(null, user)); 23 | passport.deserializeUser((obj, cb) => cb(null, obj)); 24 | 25 | // Initialize App ID authentication middleware 26 | authRouter.use( 27 | passport.authenticate( 28 | WebAppStrategy.STRATEGY_NAME, 29 | { 30 | successRedirect: "/", 31 | forceLogin: true, 32 | }, 33 | console.log("Authenticated"), 34 | ), 35 | ); 36 | 37 | //app.use("/", express.static(path.join(__dirname, "../dist"))); 38 | 39 | module.exports = authRouter; 40 | -------------------------------------------------------------------------------- /watson-stt-stream-connector/lib/setupTelemetry.js: -------------------------------------------------------------------------------- 1 | 2 | const { NodeTracerProvider } = require('@opentelemetry/node'); 3 | const { SimpleSpanProcessor, BatchSpanProcessor } = require('@opentelemetry/tracing'); 4 | const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-proto'); 5 | const { grpc } = require('@opentelemetry/exporter-trace-otlp-grpc'); 6 | const { credentials } = require('@grpc/grpc-js'); 7 | 8 | 9 | const LOG_LEVEL = process.env.LOG_LEVEL; 10 | const logger = require('pino')({ level: LOG_LEVEL, name: 'setupTelemetry' }); 11 | 12 | let providerInstance; // Store the provider instance to be re-used 13 | 14 | function setupTelemetry() { 15 | if (!providerInstance) { 16 | const provider = new NodeTracerProvider(); 17 | provider.register(); 18 | let spanProcessor; 19 | 20 | if (process.env.TELEMETRY === 'true') { 21 | const exporter = new CollectorTraceExporter({ 22 | credentials: credentials.createInsecure(), 23 | url: 'jaeger:4317', 24 | }); 25 | 26 | spanProcessor = new BatchSpanProcessor(exporter); 27 | logger.debug('Enabling jaeger tracing'); 28 | } else { 29 | spanProcessor = new SimpleSpanProcessor(console); 30 | logger.debug('logging tracing to console'); 31 | } 32 | provider.addSpanProcessor(spanProcessor); 33 | providerInstance = provider; 34 | provider.register() 35 | } 36 | return providerInstance; 37 | } 38 | 39 | module.exports = setupTelemetry; -------------------------------------------------------------------------------- /wrapper-ui/src/hooks/useAgentIdProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | // Create a context for managing the agent ID 5 | const AgentIdContext = createContext(); 6 | 7 | // Custom hook for accessing the agent ID from the context 8 | export const useAgentId = () => useContext(AgentIdContext); 9 | 10 | // Agent ID provider component 11 | export const AgentIdProvider = ({ children }) => { 12 | const [agentId, setAgentId] = useState(null); 13 | 14 | useEffect(() => { 15 | // Parse query parameters from the URL 16 | const queryParams = new URLSearchParams(window.location.search); 17 | const agentIdParam = queryParams.get('agent_id'); 18 | 19 | if (agentIdParam) { 20 | // If "agent_id" query parameter is present, use its value 21 | setAgentId(agentIdParam); 22 | } else { 23 | // If "agent_id" parameter is not present, generate a UUID 24 | const newAgentId = uuidv4(); 25 | setAgentId(newAgentId); 26 | } 27 | }, []); 28 | 29 | // Method for setting the agent ID 30 | const setAgentIdHandler = (newAgentId) => { 31 | setAgentId(newAgentId); 32 | }; 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | // this needs to be used in a provider! -------------------------------------------------------------------------------- /utilities/mono-to-stereo-wav-converter/README.md: -------------------------------------------------------------------------------- 1 | # Mono-to-Stereo WAV Converter 2 | 3 | This project folder contains functions for operating on WAV files. It's primarily used to convert a single channel WAV file that contains mixed audio from both a caller and an agent into a dual channel WAV file with the two call participants split into left and right channels. The first speaker detected is always put into the left channel and the second speaker detected is always put into the right channel. 4 | 5 | Note that Watson STT is used to derive the speaker lables. 6 | 7 | ## Requirements 8 | - NodeJS v16 and higher. Go [here](https://nodejs.org/en/download/). 9 | - A Waton STT instance is required to perform the transcriptions. Go [here](https://cloud.ibm.com/docs/speech-to-text?topic=speech-to-text-gettingStarted) to get started. 10 | - The file being converted MUST be in the following format: WAV, mono, ulaw, 8 kHz (go [here](https://www.audacityteam.org/) if you need open source to convert the WAV file to this format) 11 | 12 | ## How to run 13 | 1. First you'll need to install a recent version of Node.js 14 | 2. Move the .env-example to .env and fill in the Watson STT related environment variables. 15 | 3. Run the converter using the following command: 16 | 17 | ``` 18 | node main.js path-to-input.wav path-to-output.wav 19 | ``` 20 | 21 | If you wish to just view the WAV headers you can run this command without setting up the .env file: 22 | ``` 23 | node wav_header_dump.js path-to-input.wav 24 | ``` 25 | 26 | ## Limitations 27 | If more than two speakers are detected, any speaker label greater than 1 is assumed to be the same speaker that is labeled 1. 28 | 29 | -------------------------------------------------------------------------------- /wrapper-ui/src/components/Dashboard/Conversation/Conversation.jsx: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect, useRef} from "react"; 2 | import {AppContext} from "../../../context/context"; 3 | import Message from "./Message/Message"; 4 | 5 | const Conversation = () => { 6 | const {currentSessionUser, conversations} = useContext(AppContext); 7 | // Ref for the scrollable area 8 | const scrollableAreaRef = useRef(null); 9 | 10 | useEffect(() => { 11 | // Scrolls to the bottom of the scrollable area 12 | if (scrollableAreaRef.current) { 13 | scrollableAreaRef.current.scrollTop = 14 | scrollableAreaRef.current.scrollHeight; 15 | } 16 | }, [conversations]); 17 | 18 | return ( 19 | //
without sentiment 20 |
22 |

23 | Conversation 24 |

25 | {/*
26 |

Conversation

27 |
*/} 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 | 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 |
50 |
54 |
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 |
18 |
19 |
20 | 21 | logout 28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | Hi , Congratulations!
36 | You’ve made your first authentication.
37 |
38 |
What you can do next:
39 | 43 | 46 | 47 |
48 | 49 |
50 |
51 |
ID Token
52 |
53 |
54 |
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 |
51 |
55 |
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 | 36 | 40 |
41 |
42 | 45 | 46 |
47 |
48 | 51 | 52 |
53 |
54 | 57 | 58 |
59 |
60 | 63 | 64 |
65 |
66 | 67 | 68 | 71 | 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 | ![Architecture](./images/aa-arch.png) 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 | ![Celery Fan](./images/celery-fan.png) 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 | --------------------------------------------------------------------------------