platform/services/ai-bot/pod-ai-bot/src/controller.ts
Kristina 94f8b9f846
Love agent updates (#7153)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
2024-11-11 20:22:38 +07:00

311 lines
10 KiB
TypeScript

//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
aiBotAccountEmail,
AIEventRequest,
AIEventType,
AIMessageEventRequest,
AITransferEventRequest,
ConnectMeetingRequest,
DisconnectMeetingRequest,
IdentityResponse,
OnboardingEvent,
OnboardingEventRequest,
OpenChatInSidebarData,
PostTranscriptRequest,
TranslateRequest,
TranslateResponse
} from '@hcengineering/ai-bot'
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import OpenAI from 'openai'
import { encodingForModel } from 'js-tiktoken'
import { htmlToMarkup, markupToHTML } from '@hcengineering/text'
import { Markup, MeasureContext, Ref, WorkspaceId } from '@hcengineering/core'
import { Room } from '@hcengineering/love'
import { WorkspaceClient } from './workspace/workspaceClient'
import config from './config'
import { DbStorage } from './storage'
import { SupportWsClient } from './workspace/supportWsClient'
import { AIReplyTransferData } from './types'
import { tryAssignToWorkspace } from './utils/account'
import { translateHtml } from './utils/openai'
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
export class AIControl {
private readonly workspaces: Map<string, WorkspaceClient> = new Map<string, WorkspaceClient>()
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
private readonly connectingWorkspaces: Set<string> = new Set<string>()
readonly aiClient?: OpenAI
readonly encoding = encodingForModel(config.OpenAIModel)
supportClient: SupportWsClient | undefined = undefined
constructor (
readonly storage: DbStorage,
private readonly ctx: MeasureContext
) {
this.aiClient =
config.OpenAIKey !== ''
? new OpenAI({
apiKey: config.OpenAIKey,
baseURL: config.OpenAIBaseUrl === '' ? undefined : config.OpenAIBaseUrl
})
: undefined
void this.connectSupportWorkspace()
}
async getWorkspaceRecord (workspace: string): Promise<WorkspaceInfoRecord> {
return (await this.storage.getWorkspace(workspace)) ?? { workspace: config.SupportWorkspace }
}
async connectSupportWorkspace (): Promise<void> {
if (this.supportClient === undefined && !this.connectingWorkspaces.has(config.SupportWorkspace)) {
this.connectingWorkspaces.add(config.SupportWorkspace)
const record = await this.getWorkspaceRecord(config.SupportWorkspace)
this.supportClient = (await this.createWorkspaceClient(config.SupportWorkspace, record)) as SupportWsClient
this.connectingWorkspaces.delete(config.SupportWorkspace)
}
}
async closeWorkspaceClient (workspace: string): Promise<void> {
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
this.closeWorkspaceTimeouts.delete(workspace)
}
const client = this.workspaces.get(workspace)
if (client !== undefined) {
if (client.canClose()) {
await client.close()
this.workspaces.delete(workspace)
} else {
this.updateClearInterval(workspace)
}
}
this.connectingWorkspaces.delete(workspace)
}
updateClearInterval (workspace: string): void {
const newTimeoutId = setTimeout(() => {
void this.closeWorkspaceClient(workspace)
}, CLOSE_INTERVAL_MS)
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
}
async createWorkspaceClient (workspace: string, info: WorkspaceInfoRecord): Promise<WorkspaceClient | undefined> {
const isAssigned = await tryAssignToWorkspace(workspace, this.ctx)
if (!isAssigned) {
return
}
const token = generateToken(aiBotAccountEmail, { name: workspace })
const endpoint = await getTransactorEndpoint(token)
this.ctx.info('Listen workspace: ', { workspace })
if (workspace === config.SupportWorkspace) {
return new SupportWsClient(endpoint, token, workspace, this, this.ctx.newChild(workspace, {}), info)
}
return new WorkspaceClient(endpoint, token, workspace, this, this.ctx.newChild(workspace, {}), info)
}
async initWorkspaceClient (workspace: string): Promise<void> {
if (workspace === config.SupportWorkspace) {
return
}
this.connectingWorkspaces.add(workspace)
if (!this.workspaces.has(workspace)) {
const record = await this.getWorkspaceRecord(workspace)
const client = await this.createWorkspaceClient(workspace, record)
if (client === undefined) {
this.connectingWorkspaces.delete(workspace)
return
}
this.workspaces.set(workspace, client)
}
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
}
this.updateClearInterval(workspace)
this.connectingWorkspaces.delete(workspace)
}
allowAiReplies (workspace: string, email: string): boolean {
if (this.supportClient === undefined) return true
return this.supportClient.allowAiReplies(workspace, email)
}
async transferAIReplyToSupport (response: Markup, data: AIReplyTransferData): Promise<void> {
if (this.supportClient === undefined) return
await this.supportClient.transferAIReply(response, data)
}
async transfer (event: AITransferEventRequest): Promise<void> {
const workspace = event.toWorkspace
if (workspace === config.SupportWorkspace) {
if (this.supportClient === undefined) return
await this.supportClient.transfer(event)
return
}
const wsClient = await this.getWorkspaceClient(workspace)
if (wsClient === undefined) return
await wsClient.transfer(event)
}
async close (): Promise<void> {
for (const workspace of this.workspaces.values()) {
await workspace.close()
}
for (const timeoutId of this.closeWorkspaceTimeouts.values()) {
clearTimeout(timeoutId)
}
this.workspaces.clear()
}
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
const record = await this.storage.getWorkspace(workspace)
if (record === undefined) {
await this.storage.addWorkspace({ workspace, avatarPath: path, avatarLastModified: lastModified })
} else {
await this.storage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } })
}
}
async getWorkspaceClient (workspace: string): Promise<WorkspaceClient | undefined> {
await this.initWorkspaceClient(workspace)
return this.workspaces.get(workspace)
}
async openChatInSidebar (data: OpenChatInSidebarData): Promise<void> {
const wsClient = await this.getWorkspaceClient(data.workspace)
if (wsClient === undefined) return
await wsClient.openAIChatInSidebar(data.email)
}
async processOnboardingEvent (event: OnboardingEventRequest): Promise<void> {
switch (event.event) {
case OnboardingEvent.OpenChatInSidebar:
await this.openChatInSidebar(event.data as OpenChatInSidebarData)
break
}
}
async translate (req: TranslateRequest): Promise<TranslateResponse | undefined> {
if (this.aiClient === undefined) {
return undefined
}
const html = markupToHTML(req.text)
const result = await translateHtml(this.aiClient, html, req.lang)
const text = result !== undefined ? htmlToMarkup(result) : req.text
return {
text,
lang: req.lang
}
}
async processMessageEvent (workspace: string, event: AIMessageEventRequest): Promise<void> {
const wsClient = await this.getWorkspaceClient(workspace)
if (wsClient === undefined) return
await wsClient.processMessageEvent(event)
}
async processEvent (workspace: string, events: AIEventRequest[]): Promise<void> {
for (const event of events) {
switch (event.type) {
case AIEventType.Transfer:
await this.transfer(event as AITransferEventRequest)
break
case AIEventType.Message:
await this.processMessageEvent(workspace, event as AIMessageEventRequest)
break
default:
this.ctx.warn('unknown event', event)
break
}
}
}
async connect (workspace: string): Promise<void> {
await this.initWorkspaceClient(workspace)
}
async loveConnect (workspace: WorkspaceId, request: ConnectMeetingRequest): Promise<void> {
const wsClient = await this.getWorkspaceClient(workspace.name)
if (wsClient === undefined) return
await wsClient.loveConnect(request)
}
async loveDisconnect (workspace: WorkspaceId, request: DisconnectMeetingRequest): Promise<void> {
const wsClient = await this.getWorkspaceClient(workspace.name)
if (wsClient === undefined) return
await wsClient.loveDisconnect(request)
}
async getLoveIdentity (roomName: string): Promise<IdentityResponse | undefined> {
const parsed = roomName.split('_')
const workspace = parsed[0]
if (workspace === null) return
const wsClient = await this.getWorkspaceClient(workspace)
if (wsClient === undefined) return
return await wsClient.getLoveIdentity()
}
async processLoveTranscript (request: PostTranscriptRequest): Promise<void> {
const parsed = request.roomName.split('_')
const workspace = parsed[0]
const roomId = parsed[parsed.length - 1]
if (workspace === null || roomId === null) return
const wsClient = await this.getWorkspaceClient(workspace)
if (wsClient === undefined) return
await wsClient.processLoveTranscript(request.transcript, request.participant, roomId as Ref<Room>, request.final)
}
}