Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin helpdesk #2922

Draft
wants to merge 41 commits into
base: master-lts
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7067888
chore(botonic): remove botonic-api from 1.0.0-dev.1
Iru89 Feb 9, 2024
458387f
chore(botonic): remove botonic-nlp package
Iru89 Feb 9, 2024
5735701
chore(botonic): remove botonic-intent-classification package
Iru89 Feb 9, 2024
b24d3d7
chore(botonic): remove botonic-ner package
Iru89 Feb 9, 2024
92ece07
chore(botonic): remove botonic-pulumi package
Iru89 Feb 9, 2024
402c82e
chore(botonic): remove create-botonic-app package
Iru89 Feb 9, 2024
b5c64fe
chore(botonic): remove package-lock.json of all packages
Iru89 Feb 9, 2024
3b20024
chore(botonic): create a npm monorepo, tsconfig files to build as cjs…
Iru89 Feb 9, 2024
551e087
chore(botonic-core): update botonic-core to Node20 using the monorepo
Iru89 Feb 9, 2024
28d17d5
chore(botonic-react): update botonic-react to Node20 using the monorepo
Iru89 Feb 9, 2024
6811659
chore(plugin-flow-builder): update plugin-flow-builder to Node20 usin…
Iru89 Feb 9, 2024
a4b6cd6
chore(plugin-knowledge-bases): update plugin-knowledge-bases to Node2…
Iru89 Feb 9, 2024
1d9f24b
chore(plugin-hubtype-babel): update plugin-hubtype-babel to Node20 us…
Iru89 Feb 9, 2024
e661433
chore(plugin-hubtype-analytics): update plugin-hubtype-analytics to N…
Iru89 Feb 9, 2024
b82732d
chore(plugin-watson): update plugin-watson to Node20 using the monorepo
Iru89 Feb 13, 2024
6cd0c6b
chore(plugin-segment): update plugin-segment to Node20 using the mono…
Iru89 Feb 13, 2024
d5d5e2f
chore(plugin-luis): update plugin-luis to Node20 using the monorepo
Iru89 Feb 13, 2024
6ed6697
chore(plugin-inbenta): update plugin-inbenta to Node20 using the mono…
Iru89 Feb 13, 2024
dc9290f
chore(plugin-google-analytics): update plugin-google-analytics to Nod…
Iru89 Feb 13, 2024
b5541b9
chore(plugin-dialowflow): update plugin-dialogflow to Node20 using th…
Iru89 Feb 13, 2024
f0d84a3
chore(plugin-dynamodb): update plugin-dynamodb to Node20 using the mo…
Iru89 Feb 13, 2024
05a572e
chore(plugin-google-translation): update plugin-google-translation to…
Iru89 Feb 13, 2024
586deb7
chore(plugin-dashbot): update plugin-dashbot to Node20 using the mono…
Iru89 Feb 13, 2024
48d26c3
chore(plugin-contentful): update plugin-contentful to Node20 using th…
Iru89 Feb 20, 2024
c737768
chore(eslint-config): update eslint-config packages versions to use …
Iru89 Feb 20, 2024
723eb27
chore(botonic-dx): update botonic-dx packages versions to use Node20 …
Iru89 Feb 20, 2024
57c7a6f
chore(botonic): fix lint errors
Iru89 Feb 20, 2024
ac30643
chore(botonic): update root package, tsconfig.base.json, remove lerna…
Iru89 Feb 20, 2024
b79dd91
chore(botonic): remove github actions for removed packages
Iru89 Feb 22, 2024
383b7ec
chore(botonic): update to node 20 github actions
Iru89 Feb 22, 2024
8c51772
fix(botonic-react): avoid access to undefined atributs
Iru89 Feb 22, 2024
6b27c8b
chore(botonic): publish new alpha version for all packages and add np…
Iru89 Feb 22, 2024
541a59a
refactor(plugin-flow-builder): change crypto for uuid to be able to r…
Iru89 Feb 27, 2024
caf9537
feat(botonic-examples): add examples in botonic monorepo
Iru89 Mar 2, 2024
8f28111
chore(botonic-cli): update botonic-cli to Node20 using the monorepoan…
Iru89 Mar 4, 2024
c6e8f32
chore(botonic-dx): upgrade dependencies in botonic-dx and botonic-esl…
Iru89 Mar 4, 2024
d26d582
chore(plugin-contentful): add tsconfig for tests
Iru89 Mar 4, 2024
e1e4013
chore(plugin): update github actions using monorepo and pre-commit-co…
Iru89 Mar 4, 2024
2f24740
chore(botonic): remove preinstall script and lint_ci for all packages
Iru89 Mar 6, 2024
0460104
wip
CarlosELorenzo Mar 28, 2024
483494c
WIP
CarlosELorenzo Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
CarlosELorenzo committed Mar 28, 2024
commit 0460104f1d7462e62bf105c59343dbef236b67f8
11,037 changes: 0 additions & 11,037 deletions docs/package-lock.json

This file was deleted.

204 changes: 114 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions packages/botonic-plugin-hubtype-helpdesk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@botonic/plugin-hubtype-helpdesk",
"version": "0.25.0-alpha.0",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"description": "Use hubtype helpdesk apis.",
"scripts": {
"build": "rm -rf lib && ../../node_modules/.bin/tsc -p tsconfig.json && ../../node_modules/.bin/tsc -p tsconfig.esm.json",
"build:watch": "npm run build -- --watch",
"test": "echo \"Error: no test specified\" && exit 1",
"prepublishOnly": "rm -rf lib && npm i && npm run build",
"lint": "npm run lint_core -- --fix",
"lint_core": "../../node_modules/.bin/eslint_d --cache --quiet 'src/**/*.ts*'"
},
"dependencies": {
"@botonic/core": "0.25.0-alpha.4",
"axios": "^1.6.7"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hubtype/botonic.git"
},
"author": "",
"bugs": {
"url": "https://github.com/hubtype/botonic/issues"
},
"files": [
"lib/**",
"src/**",
"README.md"
],
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"bot-framework",
"chatbot",
"hubtype-helpdesk",
"conversational-app",
"conversational-ui",
"javascript",
"typescript"
],
"eslintConfig": {
"extends": "../.eslintrc.js",
"root": true
}
}

75 changes: 75 additions & 0 deletions packages/botonic-plugin-hubtype-helpdesk/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import axios from 'axios'

export type ApiOptions = {
timeout?: number
}

enum Methods {
GET = 'get',
POST = 'post',
DELETE = 'delete',
PUT = 'put',
}

export const DEFAULT_API_CALL_TIMEOUT_MS = 28000

export default class Api {
protected headers: Record<string, string>
protected readonly timeout

constructor(options?: ApiOptions) {
this.headers = {
'Content-Type': 'application/json',
}
this.timeout = options?.timeout ?? DEFAULT_API_CALL_TIMEOUT_MS
}

async get<T>(
endpoint: string,
params?: Record<string, any>,
headers?: Record<string, string>
): Promise<T> {
return this.doCall<T>(endpoint, Methods.GET, params, headers)
}

async post<T>(
endpoint: string,
data: Record<string, any>,
headers?: Record<string, string>
): Promise<T> {
return this.doCall<T>(endpoint, Methods.POST, data, headers)
}

async delete<T>(
endpoint: string,
data?: Record<string, any>,
headers?: Record<string, string>
): Promise<T> {
return this.doCall<T>(endpoint, Methods.DELETE, data, headers)
}

async put<T>(
endpoint: string,
data?: Record<string, any>,
headers?: Record<string, string>
): Promise<T> {
return this.doCall<T>(endpoint, Methods.PUT, data, headers)
}

protected async doCall<T>(
endpoint: string,
method: Methods,
data?: Record<string, any>,
headers?: Record<string, string>
): Promise<T> {
const resp = await axios({
method: method,
url: endpoint,
params: method === Methods.POST ? undefined : data,
data: method === Methods.POST ? data : undefined,
headers: { ...this.headers, ...headers },
timeout: this.timeout,
})
return resp.data
}
}
240 changes: 240 additions & 0 deletions packages/botonic-plugin-hubtype-helpdesk/src/desk/desk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { HubtypeSession } from '@botonic/core'
import { ActionRequest } from '@botonic/react'
import axios from 'axios'

import { BotonicQueue } from '../queue'
import {
ApiDeskQueue,
AvailableAgent,
DeskCase,
DeskInterface,
DeskOptions,
GetDeskCaseListParams,
RequestLimits,
} from './types'

const BASE_URL = 'https://api.hubtype.com'
const DEFAULT_REQUEST_LIMITS: RequestLimits = {
queues: '200',
}

export class Desk implements DeskInterface {
private session!: HubtypeSession
private readonly requestLimits: RequestLimits

static getDesk(
request: ActionRequest,
options: DeskOptions | { desk: DeskInterface } = {}
): DeskInterface {
const desk = 'desk' in options ? options.desk : new Desk({ ...options })
desk.init(request)
return desk
}

constructor(options?: DeskOptions) {
this.requestLimits = options?.requestLimits || DEFAULT_REQUEST_LIMITS
}

init(request: ActionRequest): void {
this.session = request.session as HubtypeSession
}

async getQueueById(
queueId: string,
deskProjectId?: string
): Promise<BotonicQueue> {
try {
const deskQueue = (await this.getDeskQueueList()).filter(queue =>
deskProjectId
? queue.contains({ id: queueId, projectId: deskProjectId })
: queue.contains({ id: queueId })
)[0]
if (!deskQueue) {
throw new Error('Queue not found in desk')
}
return deskQueue
} catch (e) {
console.error(`Error in getQueueById with id ${queueId}`, e)
return BotonicQueue.closedEmptyQueue({ id: queueId })
}
}

async getQueueByName(
queueName: string,
deskProjectId?: string
): Promise<BotonicQueue> {
try {
const deskQueues = (await this.getDeskQueueList()).filter(deskQueue =>
deskProjectId
? deskQueue.contains({ name: queueName, projectId: deskProjectId })
: deskQueue.contains({ name: queueName })
)
if (deskQueues.length === 0) {
throw new Error('Queue not found in Desk')
}
if (deskQueues.length > 1) {
console.error(
`Multiple Desk Queues with name '${queueName}'. First one used. Specify the project ID if they are in separate projects`
)
}
return deskQueues[0]
} catch (e) {
console.error(`Error in getQueueByName with name '${queueName}'`, e)
return BotonicQueue.closedEmptyQueue({ name: queueName })
}
}

async getAvailableAgentsByQueue(queueId: string): Promise<AvailableAgent[]> {
try {
const baseUrl = this.session._hubtype_api || BASE_URL
const endpointUrl = `${baseUrl}/external/v1/queues/${queueId}/available_agents/`
const resp = await axios({
headers: {
Authorization: `Bearer ${this.session._access_token}`,
},
method: 'get',
url: endpointUrl,
})
return resp.data as AvailableAgent[]
} catch (e) {
console.error('Error getting avaiable agents from backend', e)
return []
}
}

async getAssignedAgentEmailByQueue(
queue: BotonicQueue
): Promise<string | undefined> {
try {
const availableAgents = await this.getAvailableAgentsByQueue(queue.id)
return availableAgents[0]?.email
} catch (e) {
console.error(`Error in getAssignedAgentEmailByQueue`, e)
}

return undefined
}

async getAssignedAgentByQueue(
queue: BotonicQueue
): Promise<AvailableAgent | undefined> {
try {
const availableAgents = await this.getAvailableAgentsByQueue(queue.id)
return availableAgents[0]
} catch (e) {
console.error(`Error in getAssignedAgentByQueue`, e)
}

return undefined
}

async isAgentAvailableInQueue(
agentEmail: string,
queueId: string
): Promise<boolean> {
const availableAgents = await this.getAvailableAgentsByQueue(queueId)
return availableAgents.some(
availableAgent => availableAgent.email === agentEmail
)
}

private async getDeskQueueList(page = 1): Promise<BotonicQueue[]> {
try {
let queues: BotonicQueue[] = []
const baseUrl = this.session._hubtype_api || BASE_URL
const pageSize = this.requestLimits.queues
const endpointUrl = `${baseUrl}/external/v1/queues?page=${page}&fields=id,name,status,project_id&page_size=${pageSize}`
console.log('queueList', {
headers: {
Authorization: `Bearer ${this.session._access_token}`,
},
method: 'get',
url: endpointUrl,
data: { bot_id: this.session.bot.id },
})
const resp = await axios({
headers: {
Authorization: `Bearer ${this.session._access_token}`,
},
method: 'get',
url: endpointUrl,
data: { bot_id: this.session.bot.id },
})
if (resp.data.next) {
const nextPage = page + 1
queues = queues.concat(await this.getDeskQueueList(nextPage))
}

queues = queues.concat(
resp.data.results.map(
(deskQueue: ApiDeskQueue) => new BotonicQueue(deskQueue)
) as ConcatArray<BotonicQueue>
)
return queues
} catch (e) {
throw new Error(`Error getting queues from backend: ${e as string}`)
}
}

async getDeskCaseList(params: GetDeskCaseListParams): Promise<DeskCase[]> {
try {
const baseUrl = this.session._hubtype_api || BASE_URL
const endpointUrl = `${baseUrl}/external/v1/cases/search`
const queryParams = this.getCaseSearchQueryParams(params)
const resp = await axios({
headers: {
Authorization: `Bearer ${this.session._access_token}`,
},
method: 'post',
url: endpointUrl,
params: queryParams,
})
return resp.data.results
} catch (e) {
console.error('Error getting cases from backend', e)
return []
}
}

private getCaseSearchQueryParams({
searchBy,
startDate,
endDate,
status = [],
provider = [],
queueId = [],
projectId = [],
agentId = [],
cursor,
pageSize,
}: GetDeskCaseListParams): URLSearchParams {
const urlParams = new URLSearchParams()

if (searchBy) {
urlParams.append('q', searchBy)
}
if (startDate) {
const startDateParam = startDate.toISOString().replace('Z', '+00:00')
urlParams.append('start_date', startDateParam)
}
if (endDate) {
const endDateParam = endDate.toISOString().replace('Z', '+00:00')
urlParams.append('end_date', endDateParam)
}

status.forEach(status => urlParams.append('status', status))
provider.forEach(provider => urlParams.append('provider', provider))
queueId.forEach(queueId => urlParams.append('queue_id', queueId))
projectId.forEach(projectId => urlParams.append('project_id', projectId))
agentId.forEach(agentId => urlParams.append('agent_id', agentId))

if (cursor) {
urlParams.append('cursor', cursor)
}
if (pageSize) {
urlParams.append('page_size', String(pageSize))
}

return urlParams
}
}
25 changes: 25 additions & 0 deletions packages/botonic-plugin-hubtype-helpdesk/src/desk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ActionRequest } from '@botonic/react'

import { Desk } from './desk'
import { BotonicDeskOptions, DeskInterface } from './types'

export * from './desk'
export * from './types'

export default class BotonicDesk {
static getDesk(
request: ActionRequest,
options: BotonicDeskOptions | { desk: DeskInterface } = {}
): DeskInterface {
const desk = 'desk' in options ? options.desk : new Desk({ ...options })
desk.init(request)
return desk
}
}

// let desk
// if ('desk' in options) {
// desk = options.desk
// } else {
// desk = new Desk({ ...options })
// }
Loading