mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-11 12:57:59 +00:00
Use rest api instead of mongo adapter (#7063)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
67f9967551
commit
12c460c00b
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -54,7 +54,8 @@
|
|||||||
"MODEL_VERSION": "0.6.287",
|
"MODEL_VERSION": "0.6.287",
|
||||||
// "VERSION": "0.6.289",
|
// "VERSION": "0.6.289",
|
||||||
"ELASTIC_INDEX_NAME": "local_storage_index",
|
"ELASTIC_INDEX_NAME": "local_storage_index",
|
||||||
"UPLOAD_URL": "/files",
|
"UPLOAD_URL": "/files",
|
||||||
|
"AI_BOT_URL": "http://localhost:4010"
|
||||||
},
|
},
|
||||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||||
"runtimeVersion": "20",
|
"runtimeVersion": "20",
|
||||||
|
@ -257,6 +257,7 @@ services:
|
|||||||
- ELASTIC_INDEX_NAME=local_storage_index
|
- ELASTIC_INDEX_NAME=local_storage_index
|
||||||
- BRANDING_PATH=/var/cfg/branding.json
|
- BRANDING_PATH=/var/cfg/branding.json
|
||||||
- SUPPORT_WORKSPACE=support
|
- SUPPORT_WORKSPACE=support
|
||||||
|
- AI_BOT_URL=http://host.docker.internal:4010
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
transactor_pg:
|
transactor_pg:
|
||||||
image: hardcoreeng/transactor
|
image: hardcoreeng/transactor
|
||||||
@ -384,6 +385,7 @@ services:
|
|||||||
- AVATAR_PATH=./avatar.png
|
- AVATAR_PATH=./avatar.png
|
||||||
- AVATAR_CONTENT_TYPE=.png
|
- AVATAR_CONTENT_TYPE=.png
|
||||||
- STATS_URL=http://host.docker.internal:4900
|
- STATS_URL=http://host.docker.internal:4900
|
||||||
|
# - OPENAI_API_KEY=token
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
@ -13,18 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Builder, Mixin, Model, Prop, TypeRef, TypeString } from '@hcengineering/model'
|
import { type Builder, Mixin } from '@hcengineering/model'
|
||||||
import core, { type Account, type Class, type Doc, type Domain, type Ref, type Space } from '@hcengineering/core'
|
import core, { type Domain, type Ref } from '@hcengineering/core'
|
||||||
import serverCore from '@hcengineering/server-core'
|
import serverCore from '@hcengineering/server-core'
|
||||||
import serverAiBot from '@hcengineering/server-ai-bot'
|
import serverAiBot from '@hcengineering/server-ai-bot'
|
||||||
import { TDoc } from '@hcengineering/model-core'
|
import aiBot, { type TransferredMessage } from '@hcengineering/ai-bot'
|
||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
|
||||||
import aiBot, {
|
|
||||||
type AIBotEvent,
|
|
||||||
type AIBotTransferEvent,
|
|
||||||
type AIBotResponseEvent,
|
|
||||||
type TransferredMessage
|
|
||||||
} from '@hcengineering/ai-bot'
|
|
||||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { TChatMessage } from '@hcengineering/model-chunter'
|
import { TChatMessage } from '@hcengineering/model-chunter'
|
||||||
@ -33,48 +26,6 @@ export { serverAiBotId } from '@hcengineering/server-ai-bot'
|
|||||||
|
|
||||||
export const DOMAIN_AI_BOT = 'ai_bot' as Domain
|
export const DOMAIN_AI_BOT = 'ai_bot' as Domain
|
||||||
|
|
||||||
@Model(aiBot.class.AIBotEvent, core.class.Doc, DOMAIN_AI_BOT)
|
|
||||||
export class TAIBotEvent extends TDoc implements AIBotEvent {
|
|
||||||
@Prop(TypeRef(chunter.class.ChatMessage), core.string.Class)
|
|
||||||
messageClass!: Ref<Class<ChatMessage>>
|
|
||||||
|
|
||||||
@Prop(TypeRef(chunter.class.ChatMessage), core.string.Ref)
|
|
||||||
messageId!: Ref<ChatMessage>
|
|
||||||
|
|
||||||
@Prop(TypeString(), getEmbeddedLabel('Collection'))
|
|
||||||
collection!: string
|
|
||||||
|
|
||||||
@Prop(TypeString(), getEmbeddedLabel('Message'))
|
|
||||||
message!: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model(aiBot.class.AIBotResponseEvent, aiBot.class.AIBotEvent)
|
|
||||||
export class TAIBotResponseEvent extends TAIBotEvent implements AIBotResponseEvent {
|
|
||||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
|
||||||
objectId!: Ref<Doc>
|
|
||||||
|
|
||||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
|
||||||
objectClass!: Ref<Class<Doc>>
|
|
||||||
|
|
||||||
@Prop(TypeRef(core.class.Space), core.string.Space)
|
|
||||||
objectSpace!: Ref<Space>
|
|
||||||
|
|
||||||
@Prop(TypeRef(core.class.Account), core.string.Account)
|
|
||||||
user!: Ref<Account>
|
|
||||||
|
|
||||||
email!: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model(aiBot.class.AIBotTransferEvent, aiBot.class.AIBotEvent)
|
|
||||||
export class TAIBotTransferEvent extends TAIBotEvent implements AIBotTransferEvent {
|
|
||||||
toEmail!: string
|
|
||||||
toWorkspace!: string
|
|
||||||
fromWorkspace!: string
|
|
||||||
fromWorkspaceName!: string
|
|
||||||
fromWorkspaceUrl!: string
|
|
||||||
parentMessageId?: Ref<ChatMessage>
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mixin(aiBot.mixin.TransferredMessage, chunter.class.ChatMessage)
|
@Mixin(aiBot.mixin.TransferredMessage, chunter.class.ChatMessage)
|
||||||
export class TTransferredMessage extends TChatMessage implements TransferredMessage {
|
export class TTransferredMessage extends TChatMessage implements TransferredMessage {
|
||||||
messageId!: Ref<ChatMessage>
|
messageId!: Ref<ChatMessage>
|
||||||
@ -82,7 +33,7 @@ export class TTransferredMessage extends TChatMessage implements TransferredMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(TAIBotEvent, TAIBotTransferEvent, TAIBotResponseEvent, TTransferredMessage)
|
builder.createModel(TTransferredMessage)
|
||||||
|
|
||||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||||
trigger: serverAiBot.trigger.OnMessageSend,
|
trigger: serverAiBot.trigger.OnMessageSend,
|
||||||
|
@ -13,41 +13,18 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Account, Class, Doc, type Mixin, Ref, Space } from '@hcengineering/core'
|
import { Account, type Mixin, Ref } from '@hcengineering/core'
|
||||||
import type { Metadata, Plugin } from '@hcengineering/platform'
|
import type { Metadata, Plugin } from '@hcengineering/platform'
|
||||||
import { plugin } from '@hcengineering/platform'
|
import { plugin } from '@hcengineering/platform'
|
||||||
import { ChatMessage } from '@hcengineering/chunter'
|
import { ChatMessage } from '@hcengineering/chunter'
|
||||||
|
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './rest'
|
||||||
|
|
||||||
export const aiBotId = 'ai-bot' as Plugin
|
export const aiBotId = 'ai-bot' as Plugin
|
||||||
|
|
||||||
export const aiBotAccountEmail = 'huly.ai.bot@hc.engineering'
|
export const aiBotAccountEmail = 'huly.ai.bot@hc.engineering'
|
||||||
|
|
||||||
export interface AIBotEvent extends Doc {
|
|
||||||
collection: string
|
|
||||||
messageClass: Ref<Class<ChatMessage>>
|
|
||||||
messageId: Ref<ChatMessage>
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AIBotResponseEvent extends AIBotEvent {
|
|
||||||
objectId: Ref<Doc>
|
|
||||||
objectClass: Ref<Class<Doc>>
|
|
||||||
objectSpace: Ref<Space>
|
|
||||||
user: Ref<Account>
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AIBotTransferEvent extends AIBotEvent {
|
|
||||||
toEmail: string
|
|
||||||
toWorkspace: string
|
|
||||||
fromWorkspace: string
|
|
||||||
fromWorkspaceName: string
|
|
||||||
fromWorkspaceUrl: string
|
|
||||||
parentMessageId?: Ref<ChatMessage>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransferredMessage extends ChatMessage {
|
export interface TransferredMessage extends ChatMessage {
|
||||||
messageId: Ref<ChatMessage>
|
messageId: Ref<ChatMessage>
|
||||||
parentMessageId?: Ref<ChatMessage>
|
parentMessageId?: Ref<ChatMessage>
|
||||||
@ -57,11 +34,6 @@ const aiBot = plugin(aiBotId, {
|
|||||||
metadata: {
|
metadata: {
|
||||||
EndpointURL: '' as Metadata<string>
|
EndpointURL: '' as Metadata<string>
|
||||||
},
|
},
|
||||||
class: {
|
|
||||||
AIBotEvent: '' as Ref<Class<AIBotEvent>>,
|
|
||||||
AIBotTransferEvent: '' as Ref<Class<AIBotTransferEvent>>,
|
|
||||||
AIBotResponseEvent: '' as Ref<Class<AIBotResponseEvent>>
|
|
||||||
},
|
|
||||||
mixin: {
|
mixin: {
|
||||||
TransferredMessage: '' as Ref<Mixin<TransferredMessage>>
|
TransferredMessage: '' as Ref<Mixin<TransferredMessage>>
|
||||||
},
|
},
|
||||||
|
58
plugins/ai-bot/src/rest.ts
Normal file
58
plugins/ai-bot/src/rest.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// 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 { Account, Class, Doc, Markup, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||||
|
import { ChatMessage } from '@hcengineering/chunter'
|
||||||
|
|
||||||
|
export enum AIEventType {
|
||||||
|
Message = 'message',
|
||||||
|
Transfer = 'transfer'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIEventRequest {
|
||||||
|
type: AIEventType
|
||||||
|
collection: string
|
||||||
|
messageClass: Ref<Class<ChatMessage>>
|
||||||
|
messageId: Ref<ChatMessage>
|
||||||
|
message: string
|
||||||
|
createdOn: Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIMessageEventRequest extends AIEventRequest {
|
||||||
|
objectId: Ref<Doc>
|
||||||
|
objectClass: Ref<Class<Doc>>
|
||||||
|
objectSpace: Ref<Space>
|
||||||
|
user: Ref<Account>
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AITransferEventRequest extends AIEventRequest {
|
||||||
|
toEmail: string
|
||||||
|
toWorkspace: string
|
||||||
|
fromWorkspace: string
|
||||||
|
fromWorkspaceName: string
|
||||||
|
fromWorkspaceUrl: string
|
||||||
|
parentMessageId?: Ref<ChatMessage>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranslateRequest {
|
||||||
|
text: Markup
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranslateResponse {
|
||||||
|
text: Markup
|
||||||
|
lang: string
|
||||||
|
}
|
@ -13,18 +13,6 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Markup } from '@hcengineering/core'
|
|
||||||
|
|
||||||
export interface TranslateRequest {
|
|
||||||
text: Markup
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TranslateResponse {
|
|
||||||
text: Markup
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum OnboardingEvent {
|
export enum OnboardingEvent {
|
||||||
OpenChatInSidebar = 'openChatInSidebar'
|
OpenChatInSidebar = 'openChatInSidebar'
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,6 @@
|
|||||||
"@hcengineering/server-telegram": "^0.6.0",
|
"@hcengineering/server-telegram": "^0.6.0",
|
||||||
"@hcengineering/pod-telegram-bot": "^0.6.0",
|
"@hcengineering/pod-telegram-bot": "^0.6.0",
|
||||||
"@hcengineering/server-ai-bot": "^0.6.0",
|
"@hcengineering/server-ai-bot": "^0.6.0",
|
||||||
"@hcengineering/server-ai-bot-resources": "^0.6.0",
|
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"bufferutil": "^4.0.8",
|
"bufferutil": "^4.0.8",
|
||||||
"msgpackr": "^1.11.0",
|
"msgpackr": "^1.11.0",
|
||||||
|
@ -72,6 +72,7 @@ setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName)
|
|||||||
setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1')
|
setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1')
|
||||||
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
|
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
|
||||||
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
|
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
|
||||||
|
setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL)
|
||||||
|
|
||||||
const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, {
|
const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, {
|
||||||
fullTextUrl: config.elasticUrl,
|
fullTextUrl: config.elasticUrl,
|
||||||
|
@ -33,8 +33,6 @@ import {
|
|||||||
} from '@hcengineering/server-core'
|
} from '@hcengineering/server-core'
|
||||||
import { type Token } from '@hcengineering/server-token'
|
import { type Token } from '@hcengineering/server-token'
|
||||||
|
|
||||||
import { serverAiBotId } from '@hcengineering/server-ai-bot'
|
|
||||||
import { createAIBotAdapter } from '@hcengineering/server-ai-bot-resources'
|
|
||||||
import { createServerPipeline, registerServerPlugins, registerStringLoaders } from '@hcengineering/server-pipeline'
|
import { createServerPipeline, registerServerPlugins, registerStringLoaders } from '@hcengineering/server-pipeline'
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
@ -80,17 +78,7 @@ export function start (
|
|||||||
dbUrl,
|
dbUrl,
|
||||||
model,
|
model,
|
||||||
{ ...opt, externalStorage, adapterSecurity: dbUrl.startsWith('postgresql') },
|
{ ...opt, externalStorage, adapterSecurity: dbUrl.startsWith('postgresql') },
|
||||||
opt.mongoUrl !== undefined
|
{}
|
||||||
? {
|
|
||||||
serviceAdapters: {
|
|
||||||
[serverAiBotId]: {
|
|
||||||
factory: createAIBotAdapter,
|
|
||||||
db: '%ai-bot',
|
|
||||||
url: opt.mongoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
)
|
)
|
||||||
const sessionFactory = (
|
const sessionFactory = (
|
||||||
token: Token,
|
token: Token,
|
||||||
|
@ -37,19 +37,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/activity": "^0.6.0",
|
"@hcengineering/activity": "^0.6.0",
|
||||||
|
"@hcengineering/ai-bot": "^0.6.0",
|
||||||
|
"@hcengineering/analytics-collector": "^0.6.0",
|
||||||
"@hcengineering/chunter": "^0.6.20",
|
"@hcengineering/chunter": "^0.6.20",
|
||||||
"@hcengineering/contact": "^0.6.24",
|
"@hcengineering/contact": "^0.6.24",
|
||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
"@hcengineering/mongo": "^0.6.1",
|
|
||||||
"@hcengineering/notification": "^0.6.23",
|
"@hcengineering/notification": "^0.6.23",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/server-activity-resources": "^0.6.0",
|
"@hcengineering/server-activity-resources": "^0.6.0",
|
||||||
"@hcengineering/server-core": "^0.6.1",
|
|
||||||
"@hcengineering/server-templates": "^0.6.0",
|
|
||||||
"@hcengineering/templates": "^0.6.11",
|
|
||||||
"@hcengineering/ai-bot": "^0.6.0",
|
|
||||||
"@hcengineering/server-ai-bot": "^0.6.0",
|
"@hcengineering/server-ai-bot": "^0.6.0",
|
||||||
"@hcengineering/analytics-collector": "^0.6.0",
|
"@hcengineering/server-core": "^0.6.1",
|
||||||
"mongodb": "6.9.0-dev.20241016.sha.3d5bd513"
|
"@hcengineering/server-token": "^0.6.11",
|
||||||
|
"@hcengineering/server-templates": "^0.6.0",
|
||||||
|
"@hcengineering/templates": "^0.6.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 { MeasureContext, WorkspaceId } from '@hcengineering/core'
|
|
||||||
import { getMongoClient, MongoClientReference } from '@hcengineering/mongo'
|
|
||||||
import { AIBotServiceAdapter, WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
|
||||||
import { Collection, Db, MongoClient } from 'mongodb'
|
|
||||||
|
|
||||||
class AIBotAdapter implements AIBotServiceAdapter {
|
|
||||||
private readonly workspacesInfoCollection: Collection<WorkspaceInfoRecord>
|
|
||||||
private readonly db: Db
|
|
||||||
closed = false
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private readonly _client: MongoClientReference,
|
|
||||||
private readonly client: MongoClient,
|
|
||||||
private readonly _metrics: MeasureContext,
|
|
||||||
private readonly dbName: string
|
|
||||||
) {
|
|
||||||
this.db = client.db(dbName)
|
|
||||||
this.workspacesInfoCollection = this.db.collection<WorkspaceInfoRecord>('workspacesInfo')
|
|
||||||
}
|
|
||||||
|
|
||||||
async processWorkspace (workspace: WorkspaceId): Promise<void> {
|
|
||||||
if (this.closed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const existsRecord = await this.workspacesInfoCollection.findOne({
|
|
||||||
workspace: workspace.name
|
|
||||||
})
|
|
||||||
|
|
||||||
if (existsRecord != null && !existsRecord.active) {
|
|
||||||
await this.workspacesInfoCollection.updateOne({ workspace: workspace.name }, { $set: { active: true } })
|
|
||||||
} else if (existsRecord == null) {
|
|
||||||
const record: WorkspaceInfoRecord = {
|
|
||||||
workspace: workspace.name,
|
|
||||||
active: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workspacesInfoCollection.insertOne(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async close (): Promise<void> {
|
|
||||||
this.closed = true
|
|
||||||
this._client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics (): MeasureContext {
|
|
||||||
return this._metrics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAIBotAdapter (url: string, db: string, metrics: MeasureContext): Promise<any> {
|
|
||||||
const _client = getMongoClient(url)
|
|
||||||
|
|
||||||
return new AIBotAdapter(_client, await _client.getClient(), metrics, db)
|
|
||||||
}
|
|
@ -16,7 +16,6 @@
|
|||||||
import core, {
|
import core, {
|
||||||
AccountRole,
|
AccountRole,
|
||||||
AttachedDoc,
|
AttachedDoc,
|
||||||
Data,
|
|
||||||
Doc,
|
Doc,
|
||||||
Ref,
|
Ref,
|
||||||
toWorkspaceString,
|
toWorkspaceString,
|
||||||
@ -30,22 +29,17 @@ import core, {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { TriggerControl } from '@hcengineering/server-core'
|
import { TriggerControl } from '@hcengineering/server-core'
|
||||||
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||||
import aiBot, { aiBotAccountEmail, AIBotResponseEvent } from '@hcengineering/ai-bot'
|
import aiBot, {
|
||||||
import { AIBotServiceAdapter, serverAiBotId } from '@hcengineering/server-ai-bot'
|
aiBotAccountEmail,
|
||||||
|
AIEventType,
|
||||||
|
AIMessageEventRequest,
|
||||||
|
AITransferEventRequest
|
||||||
|
} from '@hcengineering/ai-bot'
|
||||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||||
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||||
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||||
import { getSupportWorkspaceId } from './utils'
|
|
||||||
|
|
||||||
async function processWorkspace (control: TriggerControl): Promise<void> {
|
import { createAccountRequest, getSupportWorkspaceId, sendAIEvents } from './utils'
|
||||||
const adapter = control.serviceAdaptersManager.getAdapter(serverAiBotId) as AIBotServiceAdapter | undefined
|
|
||||||
|
|
||||||
if (adapter !== undefined) {
|
|
||||||
await adapter.processWorkspace(control.workspace)
|
|
||||||
} else {
|
|
||||||
console.error('Cannot find server adapter: ', serverAiBotId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise<boolean> {
|
async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise<boolean> {
|
||||||
const { members } = direct
|
const { members } = direct
|
||||||
@ -77,8 +71,10 @@ async function getMessageDoc (message: ChatMessage, control: TriggerControl): Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageData (doc: Doc, message: ChatMessage, email: string): Data<AIBotResponseEvent> {
|
function getMessageData (doc: Doc, message: ChatMessage, email: string): AIMessageEventRequest {
|
||||||
return {
|
return {
|
||||||
|
type: AIEventType.Message,
|
||||||
|
createdOn: message.createdOn ?? message.modifiedOn,
|
||||||
objectId: message.attachedTo,
|
objectId: message.attachedTo,
|
||||||
objectClass: message.attachedToClass,
|
objectClass: message.attachedToClass,
|
||||||
objectSpace: doc.space,
|
objectSpace: doc.space,
|
||||||
@ -91,8 +87,10 @@ function getMessageData (doc: Doc, message: ChatMessage, email: string): Data<AI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThreadMessageData (message: ThreadMessage, email: string): Data<AIBotResponseEvent> {
|
function getThreadMessageData (message: ThreadMessage, email: string): AIMessageEventRequest {
|
||||||
return {
|
return {
|
||||||
|
type: AIEventType.Message,
|
||||||
|
createdOn: message.createdOn ?? message.modifiedOn,
|
||||||
objectId: message.attachedTo,
|
objectId: message.attachedTo,
|
||||||
objectClass: message.attachedToClass,
|
objectClass: message.attachedToClass,
|
||||||
objectSpace: message.space,
|
objectSpace: message.space,
|
||||||
@ -105,15 +103,6 @@ function getThreadMessageData (message: ThreadMessage, email: string): Data<AIBo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createResponseEvent (
|
|
||||||
message: ChatMessage,
|
|
||||||
control: TriggerControl,
|
|
||||||
data: Data<AIBotResponseEvent>
|
|
||||||
): Promise<void> {
|
|
||||||
const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotResponseEvent, message.space, data)
|
|
||||||
await control.apply(control.ctx, [eventTx])
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise<Ref<ChatMessage> | undefined> {
|
async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise<Ref<ChatMessage> | undefined> {
|
||||||
if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) {
|
if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) {
|
||||||
return undefined
|
return undefined
|
||||||
@ -137,8 +126,8 @@ async function createTransferEvent (
|
|||||||
control: TriggerControl,
|
control: TriggerControl,
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
account: PersonAccount,
|
account: PersonAccount,
|
||||||
data: Data<AIBotResponseEvent>
|
data: AIMessageEventRequest
|
||||||
): Promise<void> {
|
): Promise<AITransferEventRequest | undefined> {
|
||||||
if (account.role !== AccountRole.Owner) {
|
if (account.role !== AccountRole.Owner) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -149,7 +138,9 @@ async function createTransferEvent (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
return {
|
||||||
|
type: AIEventType.Transfer,
|
||||||
|
createdOn: message.createdOn ?? message.modifiedOn,
|
||||||
messageClass: data.messageClass,
|
messageClass: data.messageClass,
|
||||||
message: message.message,
|
message: message.message,
|
||||||
collection: data.collection,
|
collection: data.collection,
|
||||||
@ -160,9 +151,7 @@ async function createTransferEvent (
|
|||||||
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
||||||
messageId: message._id,
|
messageId: message._id,
|
||||||
parentMessageId: await getThreadParent(control, message)
|
parentMessageId: await getThreadParent(control, message)
|
||||||
})
|
}
|
||||||
|
|
||||||
await control.apply(control.ctx, [eventTx])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise<void> {
|
async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise<void> {
|
||||||
@ -186,18 +175,18 @@ async function onBotDirectMessageSend (control: TriggerControl, message: ChatMes
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Data<AIBotResponseEvent> | undefined
|
let messageEvent: AIMessageEventRequest
|
||||||
|
|
||||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
data = getThreadMessageData(message as ThreadMessage, account.email)
|
messageEvent = getThreadMessageData(message as ThreadMessage, account.email)
|
||||||
} else {
|
} else {
|
||||||
data = getMessageData(direct, message, account.email)
|
messageEvent = getMessageData(direct, message, account.email)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createResponseEvent(message, control, data)
|
const transferEvent = await createTransferEvent(control, message, account, messageEvent)
|
||||||
await createTransferEvent(control, message, account, data)
|
const events = transferEvent !== undefined ? [messageEvent, transferEvent] : [messageEvent]
|
||||||
|
|
||||||
await processWorkspace(control)
|
await sendAIEvents(events, control.workspace, control.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSupportWorkspaceMessage (control: TriggerControl, message: ChatMessage): Promise<void> {
|
async function onSupportWorkspaceMessage (control: TriggerControl, message: ChatMessage): Promise<void> {
|
||||||
@ -222,18 +211,20 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { workspaceId, email } = channel
|
const { workspaceId, email } = channel
|
||||||
let data: Data<AIBotResponseEvent> | undefined
|
|
||||||
const account = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
const account = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
||||||
_id: (message.createdBy ?? message.modifiedBy) as Ref<PersonAccount>
|
_id: (message.createdBy ?? message.modifiedBy) as Ref<PersonAccount>
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
|
let data: AIMessageEventRequest
|
||||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
data = getThreadMessageData(message as ThreadMessage, account.email)
|
data = getThreadMessageData(message as ThreadMessage, account.email)
|
||||||
} else {
|
} else {
|
||||||
data = getMessageData(channel, message, account.email)
|
data = getMessageData(channel, message, account.email)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
const transferEvent: AITransferEventRequest = {
|
||||||
|
type: AIEventType.Transfer,
|
||||||
|
createdOn: data.createdOn,
|
||||||
messageClass: data.messageClass,
|
messageClass: data.messageClass,
|
||||||
message: message.message,
|
message: message.message,
|
||||||
collection: data.collection,
|
collection: data.collection,
|
||||||
@ -244,11 +235,9 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
|||||||
fromWorkspaceName: control.workspace.workspaceName,
|
fromWorkspaceName: control.workspace.workspaceName,
|
||||||
messageId: message._id,
|
messageId: message._id,
|
||||||
parentMessageId: await getThreadParent(control, message)
|
parentMessageId: await getThreadParent(control, message)
|
||||||
})
|
}
|
||||||
|
|
||||||
await control.apply(control.ctx, [tx])
|
await sendAIEvents([transferEvent], control.workspace, control.ctx)
|
||||||
|
|
||||||
await processWorkspace(control)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function OnMessageSend (
|
export async function OnMessageSend (
|
||||||
@ -407,7 +396,7 @@ export async function OnUserStatus (originTx: Tx, control: TriggerControl): Prom
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
await processWorkspace(control)
|
await createAccountRequest(control.workspace, control.ctx)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -421,5 +410,3 @@ export default async () => ({
|
|||||||
OnUserStatus
|
OnUserStatus
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export * from './adapter'
|
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
import serverAIBot from '@hcengineering/server-ai-bot'
|
import serverAIBot from '@hcengineering/server-ai-bot'
|
||||||
|
import { AIEventRequest } from '@hcengineering/ai-bot'
|
||||||
|
import { concatLink, MeasureContext, systemAccountEmail, WorkspaceId } from '@hcengineering/core'
|
||||||
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
|
|
||||||
export function getSupportWorkspaceId (): string | undefined {
|
export function getSupportWorkspaceId (): string | undefined {
|
||||||
const supportWorkspaceId = getMetadata(serverAIBot.metadata.SupportWorkspaceId)
|
const supportWorkspaceId = getMetadata(serverAIBot.metadata.SupportWorkspaceId)
|
||||||
@ -25,3 +28,49 @@ export function getSupportWorkspaceId (): string | undefined {
|
|||||||
|
|
||||||
return supportWorkspaceId
|
return supportWorkspaceId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendAIEvents (
|
||||||
|
events: AIEventRequest[],
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
ctx: MeasureContext
|
||||||
|
): Promise<void> {
|
||||||
|
const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? ''
|
||||||
|
|
||||||
|
if (url === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(concatLink(url, '/events'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + generateToken(systemAccountEmail, workspace),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(events)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
ctx.error('Could not send ai events', { err })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAccountRequest (workspace: WorkspaceId, ctx: MeasureContext): Promise<void> {
|
||||||
|
const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? ''
|
||||||
|
|
||||||
|
if (url === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(concatLink(url, '/connect'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + generateToken(systemAccountEmail, workspace),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
ctx.error('Could not send create ai account request', { err })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,7 +22,8 @@ export const serverAiBotId = 'server-ai-bot' as Plugin
|
|||||||
|
|
||||||
export default plugin(serverAiBotId, {
|
export default plugin(serverAiBotId, {
|
||||||
metadata: {
|
metadata: {
|
||||||
SupportWorkspaceId: '' as Metadata<string>
|
SupportWorkspaceId: '' as Metadata<string>,
|
||||||
|
EndpointURL: '' as Metadata<string>
|
||||||
},
|
},
|
||||||
trigger: {
|
trigger: {
|
||||||
OnMessageSend: '' as Resource<TriggerFunc>,
|
OnMessageSend: '' as Resource<TriggerFunc>,
|
||||||
|
@ -13,16 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { ServiceAdapter } from '@hcengineering/server-core'
|
|
||||||
import { WorkspaceId } from '@hcengineering/core'
|
|
||||||
|
|
||||||
export interface AIBotServiceAdapter extends ServiceAdapter {
|
|
||||||
processWorkspace: (workspace: WorkspaceId) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceInfoRecord {
|
export interface WorkspaceInfoRecord {
|
||||||
workspace: string
|
workspace: string
|
||||||
active: boolean
|
|
||||||
avatarPath?: string
|
avatarPath?: string
|
||||||
avatarLastModified?: number
|
avatarLastModified?: number
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
"@hcengineering/client-resources": "^0.6.27",
|
"@hcengineering/client-resources": "^0.6.27",
|
||||||
"@hcengineering/client": "^0.6.18",
|
"@hcengineering/client": "^0.6.18",
|
||||||
"@hcengineering/server-core": "^0.6.1",
|
"@hcengineering/server-core": "^0.6.1",
|
||||||
|
"@hcengineering/server-token": "^0.6.11",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,3 +21,4 @@ export default plugin
|
|||||||
export * from './account'
|
export * from './account'
|
||||||
export * from './blob'
|
export * from './blob'
|
||||||
export * from './client'
|
export * from './client'
|
||||||
|
export * from './token'
|
||||||
|
44
server/client/src/token.ts
Normal file
44
server/client/src/token.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Token, decodeToken } from '@hcengineering/server-token'
|
||||||
|
import { IncomingHttpHeaders } from 'http'
|
||||||
|
|
||||||
|
const extractCookieToken = (cookie?: string): Token | null => {
|
||||||
|
if (cookie === undefined || cookie === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = cookie.split(';')
|
||||||
|
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
|
||||||
|
if (tokenCookie === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedToken = tokenCookie.split('=')[1]
|
||||||
|
if (encodedToken === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeToken(encodedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractAuthorizationToken = (authorization?: string): Token | null => {
|
||||||
|
if (authorization === undefined || authorization === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const encodedToken = authorization.split(' ')[1]
|
||||||
|
|
||||||
|
if (encodedToken === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeToken(encodedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractToken (headers: IncomingHttpHeaders): Token | undefined {
|
||||||
|
try {
|
||||||
|
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
|
||||||
|
|
||||||
|
return token ?? undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
@ -13,10 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { isWorkspaceCreating, Markup, MeasureContext, systemAccountEmail } from '@hcengineering/core'
|
import { Markup, MeasureContext } from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
aiBotAccountEmail,
|
aiBotAccountEmail,
|
||||||
AIBotTransferEvent,
|
AIEventRequest,
|
||||||
|
AIEventType,
|
||||||
|
AIMessageEventRequest,
|
||||||
|
AITransferEventRequest,
|
||||||
OnboardingEvent,
|
OnboardingEvent,
|
||||||
OnboardingEventRequest,
|
OnboardingEventRequest,
|
||||||
OpenChatInSidebarData,
|
OpenChatInSidebarData,
|
||||||
@ -26,38 +29,30 @@ import {
|
|||||||
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
import { getTransactorEndpoint } from '@hcengineering/server-client'
|
import { getTransactorEndpoint } from '@hcengineering/server-client'
|
||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
import { WorkspaceLoginInfo } from '@hcengineering/account'
|
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
import { encodingForModel } from 'js-tiktoken'
|
import { encodingForModel } from 'js-tiktoken'
|
||||||
import { htmlToMarkup, markupToHTML } from '@hcengineering/text'
|
import { htmlToMarkup, markupToHTML } from '@hcengineering/text'
|
||||||
|
|
||||||
import { WorkspaceClient } from './workspaceClient'
|
import { WorkspaceClient } from './workspaceClient'
|
||||||
import { assignBotToWorkspace, getWorkspaceInfo } from './account'
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { DbStorage } from './storage'
|
import { DbStorage } from './storage'
|
||||||
import { SupportWsClient } from './supportWsClient'
|
import { SupportWsClient } from './supportWsClient'
|
||||||
import { AIReplyTransferData } from './types'
|
import { AIReplyTransferData } from './types'
|
||||||
|
import { tryAssignToWorkspace } from './utils/account'
|
||||||
|
import { translateHtml } from './utils/openai'
|
||||||
|
|
||||||
const POLLING_INTERVAL_MS = 5 * 1000 // 5 seconds
|
|
||||||
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
||||||
const ASSIGN_WORKSPACE_DELAY_MS = 5 * 1000 // 5 secs
|
|
||||||
const MAX_ASSIGN_ATTEMPTS = 5
|
|
||||||
|
|
||||||
export class AIBotController {
|
export class AIControl {
|
||||||
private readonly workspaces: Map<string, WorkspaceClient> = new Map<string, WorkspaceClient>()
|
private readonly workspaces: Map<string, WorkspaceClient> = new Map<string, WorkspaceClient>()
|
||||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||||
private readonly connectingWorkspaces: Set<string> = new Set<string>()
|
private readonly connectingWorkspaces: Set<string> = new Set<string>()
|
||||||
|
|
||||||
private readonly intervalId: NodeJS.Timeout
|
|
||||||
|
|
||||||
readonly aiClient?: OpenAI
|
readonly aiClient?: OpenAI
|
||||||
readonly encoding = encodingForModel(config.OpenAIModel)
|
readonly encoding = encodingForModel(config.OpenAIModel)
|
||||||
|
|
||||||
supportClient: SupportWsClient | undefined = undefined
|
supportClient: SupportWsClient | undefined = undefined
|
||||||
|
|
||||||
assignTimeout: NodeJS.Timeout | undefined
|
|
||||||
assignAttempts = 0
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
readonly storage: DbStorage,
|
readonly storage: DbStorage,
|
||||||
private readonly ctx: MeasureContext
|
private readonly ctx: MeasureContext
|
||||||
@ -70,37 +65,20 @@ export class AIBotController {
|
|||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
this.intervalId = setInterval(() => {
|
void this.connectSupportWorkspace()
|
||||||
void this.updateWorkspaceClients()
|
|
||||||
}, POLLING_INTERVAL_MS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceClients (): Promise<void> {
|
async getWorkspaceRecord (workspace: string): Promise<WorkspaceInfoRecord> {
|
||||||
const activeRecords = await this.storage.getActiveWorkspaces()
|
return (await this.storage.getWorkspace(workspace)) ?? { workspace: config.SupportWorkspace }
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectSupportWorkspace (): Promise<void> {
|
||||||
if (this.supportClient === undefined && !this.connectingWorkspaces.has(config.SupportWorkspace)) {
|
if (this.supportClient === undefined && !this.connectingWorkspaces.has(config.SupportWorkspace)) {
|
||||||
this.connectingWorkspaces.add(config.SupportWorkspace)
|
this.connectingWorkspaces.add(config.SupportWorkspace)
|
||||||
const record = await this.storage.getWorkspace(config.SupportWorkspace)
|
const record = await this.getWorkspaceRecord(config.SupportWorkspace)
|
||||||
this.supportClient = (await this.createWorkspaceClient(
|
this.supportClient = (await this.createWorkspaceClient(config.SupportWorkspace, record)) as SupportWsClient
|
||||||
config.SupportWorkspace,
|
|
||||||
record ?? { workspace: config.SupportWorkspace, active: true }
|
|
||||||
)) as SupportWsClient
|
|
||||||
this.connectingWorkspaces.delete(config.SupportWorkspace)
|
this.connectingWorkspaces.delete(config.SupportWorkspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const record of activeRecords) {
|
|
||||||
const ws = record.workspace
|
|
||||||
|
|
||||||
if (this.workspaces.has(ws)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.connectingWorkspaces.has(ws)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.initWorkspaceClient(ws, record)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeWorkspaceClient (workspace: string): Promise<void> {
|
async closeWorkspaceClient (workspace: string): Promise<void> {
|
||||||
@ -111,8 +89,6 @@ export class AIBotController {
|
|||||||
this.closeWorkspaceTimeouts.delete(workspace)
|
this.closeWorkspaceTimeouts.delete(workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storage.inactiveWorkspace(workspace)
|
|
||||||
|
|
||||||
const client = this.workspaces.get(workspace)
|
const client = this.workspaces.get(workspace)
|
||||||
|
|
||||||
if (client !== undefined) {
|
if (client !== undefined) {
|
||||||
@ -122,65 +98,18 @@ export class AIBotController {
|
|||||||
this.connectingWorkspaces.delete(workspace)
|
this.connectingWorkspaces.delete(workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getWorkspaceInfo (ws: string): Promise<WorkspaceLoginInfo | undefined> {
|
async createWorkspaceClient (workspace: string, info: WorkspaceInfoRecord): Promise<WorkspaceClient | undefined> {
|
||||||
const systemToken = generateToken(systemAccountEmail, { name: ws })
|
const isAssigned = await tryAssignToWorkspace(workspace, this.ctx)
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
try {
|
|
||||||
const info = await getWorkspaceInfo(systemToken)
|
|
||||||
|
|
||||||
if (info == null) {
|
if (!isAssigned) {
|
||||||
this.ctx.warn('Cannot find workspace info', { workspace: ws })
|
return
|
||||||
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return info
|
|
||||||
} catch (e) {
|
|
||||||
this.ctx.error('Error during get workspace info:', { e })
|
|
||||||
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async assignToWorkspace (workspace: string): Promise<void> {
|
|
||||||
clearTimeout(this.assignTimeout)
|
|
||||||
try {
|
|
||||||
const info = await this.getWorkspaceInfo(workspace)
|
|
||||||
|
|
||||||
if (info === undefined) {
|
|
||||||
void this.closeWorkspaceClient(workspace)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWorkspaceCreating(info?.mode)) {
|
|
||||||
this.ctx.info('Workspace is creating -> waiting...', { workspace })
|
|
||||||
this.assignTimeout = setTimeout(() => {
|
|
||||||
void this.assignToWorkspace(workspace)
|
|
||||||
}, ASSIGN_WORKSPACE_DELAY_MS)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await assignBotToWorkspace(workspace)
|
|
||||||
this.ctx.info('Assign to workspace result: ', { result, workspace })
|
|
||||||
} catch (e) {
|
|
||||||
this.ctx.error('Error during assign workspace:', { e })
|
|
||||||
if (this.assignAttempts < MAX_ASSIGN_ATTEMPTS) {
|
|
||||||
this.assignAttempts++
|
|
||||||
this.assignTimeout = setTimeout(() => {
|
|
||||||
void this.assignToWorkspace(workspace)
|
|
||||||
}, ASSIGN_WORKSPACE_DELAY_MS)
|
|
||||||
} else {
|
|
||||||
void this.closeWorkspaceClient(workspace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createWorkspaceClient (workspace: string, info: WorkspaceInfoRecord): Promise<WorkspaceClient> {
|
|
||||||
this.ctx.info('Listen workspace: ', { workspace })
|
|
||||||
await this.assignToWorkspace(workspace)
|
|
||||||
const token = generateToken(aiBotAccountEmail, { name: workspace })
|
const token = generateToken(aiBotAccountEmail, { name: workspace })
|
||||||
const endpoint = await getTransactorEndpoint(token)
|
const endpoint = await getTransactorEndpoint(token)
|
||||||
|
|
||||||
|
this.ctx.info('Listen workspace: ', { workspace })
|
||||||
|
|
||||||
if (workspace === config.SupportWorkspace) {
|
if (workspace === config.SupportWorkspace) {
|
||||||
return new SupportWsClient(endpoint, token, workspace, this, this.ctx.newChild(workspace, {}), info)
|
return new SupportWsClient(endpoint, token, workspace, this, this.ctx.newChild(workspace, {}), info)
|
||||||
}
|
}
|
||||||
@ -188,14 +117,19 @@ export class AIBotController {
|
|||||||
return new WorkspaceClient(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, info: WorkspaceInfoRecord): Promise<void> {
|
async initWorkspaceClient (workspace: string): Promise<void> {
|
||||||
if (workspace === config.SupportWorkspace) {
|
if (workspace === config.SupportWorkspace) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.connectingWorkspaces.add(workspace)
|
this.connectingWorkspaces.add(workspace)
|
||||||
|
|
||||||
if (!this.workspaces.has(workspace)) {
|
if (!this.workspaces.has(workspace)) {
|
||||||
const client = await this.createWorkspaceClient(workspace, info)
|
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)
|
this.workspaces.set(workspace, client)
|
||||||
}
|
}
|
||||||
@ -226,9 +160,8 @@ export class AIBotController {
|
|||||||
await this.supportClient.transferAIReply(response, data)
|
await this.supportClient.transferAIReply(response, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async transfer (event: AIBotTransferEvent): Promise<void> {
|
async transfer (event: AITransferEventRequest): Promise<void> {
|
||||||
const workspace = event.toWorkspace
|
const workspace = event.toWorkspace
|
||||||
const info = await this.storage.getWorkspace(workspace)
|
|
||||||
|
|
||||||
if (workspace === config.SupportWorkspace) {
|
if (workspace === config.SupportWorkspace) {
|
||||||
if (this.supportClient === undefined) return
|
if (this.supportClient === undefined) return
|
||||||
@ -237,25 +170,13 @@ export class AIBotController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info === undefined) {
|
const wsClient = await this.getWorkspaceClient(workspace)
|
||||||
this.ctx.error('Workspace info not found -> cannot transfer event', { workspace })
|
if (wsClient === undefined) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.initWorkspaceClient(workspace, info)
|
|
||||||
|
|
||||||
const wsClient = this.workspaces.get(workspace)
|
|
||||||
|
|
||||||
if (wsClient === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await wsClient.transfer(event)
|
await wsClient.transfer(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
async close (): Promise<void> {
|
async close (): Promise<void> {
|
||||||
clearInterval(this.intervalId)
|
|
||||||
|
|
||||||
for (const workspace of this.workspaces.values()) {
|
for (const workspace of this.workspaces.values()) {
|
||||||
await workspace.close()
|
await workspace.close()
|
||||||
}
|
}
|
||||||
@ -266,16 +187,23 @@ export class AIBotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
|
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
|
||||||
await this.storage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } })
|
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> {
|
async openChatInSidebar (data: OpenChatInSidebarData): Promise<void> {
|
||||||
const record = await this.storage.getWorkspace(data.workspace)
|
const wsClient = await this.getWorkspaceClient(data.workspace)
|
||||||
|
|
||||||
await this.initWorkspaceClient(data.workspace, record ?? { workspace: data.workspace, active: true })
|
|
||||||
|
|
||||||
const wsClient = this.workspaces.get(data.workspace)
|
|
||||||
|
|
||||||
if (wsClient === undefined) return
|
if (wsClient === undefined) return
|
||||||
await wsClient.openAIChatInSidebar(data.email)
|
await wsClient.openAIChatInSidebar(data.email)
|
||||||
}
|
}
|
||||||
@ -293,35 +221,38 @@ export class AIBotController {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const html = markupToHTML(req.text)
|
const html = markupToHTML(req.text)
|
||||||
const start = Date.now()
|
const result = await translateHtml(this.aiClient, html, req.lang)
|
||||||
const response = await this.aiClient.chat.completions.create({
|
const text = result !== undefined ? htmlToMarkup(result) : req.text
|
||||||
model: config.OpenAITranslateModel,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Your task is to translate the text into ${req.lang} while preserving the html structure and metadata`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: html
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const end = Date.now()
|
|
||||||
this.ctx.info('Translation time: ', { time: end - start })
|
|
||||||
const result = response.choices[0].message.content
|
|
||||||
const text = result !== null ? htmlToMarkup(result) : req.text
|
|
||||||
return {
|
return {
|
||||||
text,
|
text,
|
||||||
lang: req.lang
|
lang: req.lang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function wait (delay: number): Promise<void> {
|
async processMessageEvent (workspace: string, event: AIMessageEventRequest): Promise<void> {
|
||||||
await new Promise<void>((resolve) => {
|
const wsClient = await this.getWorkspaceClient(workspace)
|
||||||
setTimeout(() => {
|
if (wsClient === undefined) return
|
||||||
resolve()
|
|
||||||
}, delay)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { Client } from '@hcengineering/core'
|
|
||||||
import { createClient } from '@hcengineering/server-client'
|
|
||||||
|
|
||||||
export async function connectPlatform (token: string, endpoint: string): Promise<Client> {
|
|
||||||
return await createClient(endpoint, token)
|
|
||||||
}
|
|
@ -13,60 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
import { Token } from '@hcengineering/server-token'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
||||||
import { IncomingHttpHeaders, type Server } from 'http'
|
import { type Server } from 'http'
|
||||||
import { TranslateRequest, OnboardingEventRequest } from '@hcengineering/ai-bot'
|
import { TranslateRequest, OnboardingEventRequest, AIEventRequest } from '@hcengineering/ai-bot'
|
||||||
|
import { extractToken } from '@hcengineering/server-client'
|
||||||
|
|
||||||
import { ApiError } from './error'
|
import { ApiError } from './error'
|
||||||
import { AIBotController } from './controller'
|
import { AIControl } from '../controller'
|
||||||
|
|
||||||
const extractCookieToken = (cookie?: string): Token | null => {
|
|
||||||
if (cookie === undefined || cookie === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookies = cookie.split(';')
|
|
||||||
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
|
|
||||||
if (tokenCookie === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedToken = tokenCookie.split('=')[1]
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractAuthorizationToken = (authorization?: string): Token | null => {
|
|
||||||
if (authorization === undefined || authorization === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const encodedToken = authorization.split(' ')[1]
|
|
||||||
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractToken = (headers: IncomingHttpHeaders): Token => {
|
|
||||||
try {
|
|
||||||
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
|
|
||||||
|
|
||||||
if (token === null) {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token
|
|
||||||
} catch {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
||||||
|
|
||||||
@ -76,8 +31,11 @@ const handleRequest = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
const token = extractToken(req.headers)
|
||||||
|
if (token === undefined) {
|
||||||
|
throw new ApiError(401)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const token = extractToken(req.headers)
|
|
||||||
await fn(req, res, token, next)
|
await fn(req, res, token, next)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
next(err)
|
next(err)
|
||||||
@ -88,7 +46,7 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n
|
|||||||
void handleRequest(fn, req, res, next)
|
void handleRequest(fn, req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createServer (controller: AIBotController): Express {
|
export function createServer (controller: AIControl): Express {
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
@ -109,6 +67,32 @@ export function createServer (controller: AIBotController): Express {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/connect',
|
||||||
|
wrapRequest(async (_, res, token) => {
|
||||||
|
await controller.connect(token.workspace.name)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
res.json({})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/events',
|
||||||
|
wrapRequest(async (req, res, token) => {
|
||||||
|
if (req.body == null) {
|
||||||
|
throw new ApiError(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = Array.isArray(req.body) ? req.body : [req.body]
|
||||||
|
|
||||||
|
await controller.processEvent(token.workspace.name, events as AIEventRequest[])
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
res.json({})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
'/onboarding',
|
'/onboarding',
|
||||||
wrapRequest(async (req, res) => {
|
wrapRequest(async (req, res) => {
|
@ -17,14 +17,14 @@ import { setMetadata } from '@hcengineering/platform'
|
|||||||
import serverAiBot from '@hcengineering/server-ai-bot'
|
import serverAiBot from '@hcengineering/server-ai-bot'
|
||||||
import serverClient from '@hcengineering/server-client'
|
import serverClient from '@hcengineering/server-client'
|
||||||
import serverToken from '@hcengineering/server-token'
|
import serverToken from '@hcengineering/server-token'
|
||||||
|
|
||||||
import { initStatisticsContext } from '@hcengineering/server-core'
|
import { initStatisticsContext } from '@hcengineering/server-core'
|
||||||
import { createBotAccount } from './account'
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { AIBotController } from './controller'
|
import { AIControl } from './controller'
|
||||||
import { registerLoaders } from './loaders'
|
import { registerLoaders } from './loaders'
|
||||||
import { createServer, listen } from './server'
|
|
||||||
import { closeDB, DbStorage, getDB } from './storage'
|
import { closeDB, DbStorage, getDB } from './storage'
|
||||||
|
import { createBotAccount } from './utils/account'
|
||||||
|
import { createServer, listen } from './server/server'
|
||||||
|
|
||||||
export const start = async (): Promise<void> => {
|
export const start = async (): Promise<void> => {
|
||||||
setMetadata(serverToken.metadata.Secret, config.ServerSecret)
|
setMetadata(serverToken.metadata.Secret, config.ServerSecret)
|
||||||
@ -49,7 +49,7 @@ export const start = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
}
|
}
|
||||||
const aiController = new AIBotController(storage, ctx)
|
const aiController = new AIControl(storage, ctx)
|
||||||
const app = createServer(aiController)
|
const app = createServer(aiController)
|
||||||
const server = listen(app, config.Port)
|
const server = listen(app, config.Port)
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
import { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
import { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
||||||
import { Collection, Db, MongoClient, ObjectId, UpdateFilter, WithId } from 'mongodb'
|
import { Collection, Db, MongoClient, ObjectId, UpdateFilter, WithId } from 'mongodb'
|
||||||
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
|
||||||
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { HistoryRecord } from './types'
|
import { HistoryRecord } from './types'
|
||||||
@ -61,18 +61,14 @@ export class DbStorage {
|
|||||||
await this.historyCollection.deleteMany({ _id: { $in: _ids } })
|
await this.historyCollection.deleteMany({ _id: { $in: _ids } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveWorkspaces (): Promise<WorkspaceInfoRecord[]> {
|
|
||||||
return await this.workspacesInfoCollection.find({ active: true }).toArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
async inactiveWorkspace (workspace: string): Promise<void> {
|
|
||||||
await this.workspacesInfoCollection.updateOne({ workspace }, { $set: { active: false } })
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWorkspace (workspace: string): Promise<WorkspaceInfoRecord | undefined> {
|
async getWorkspace (workspace: string): Promise<WorkspaceInfoRecord | undefined> {
|
||||||
return (await this.workspacesInfoCollection.findOne({ workspace })) ?? undefined
|
return (await this.workspacesInfoCollection.findOne({ workspace })) ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addWorkspace (record: WorkspaceInfoRecord): Promise<void> {
|
||||||
|
await this.workspacesInfoCollection.insertOne(record)
|
||||||
|
}
|
||||||
|
|
||||||
async updateWorkspace (workspace: string, update: UpdateFilter<WorkspaceInfoRecord>): Promise<void> {
|
async updateWorkspace (workspace: string, update: UpdateFilter<WorkspaceInfoRecord>): Promise<void> {
|
||||||
await this.workspacesInfoCollection.updateOne({ workspace }, update)
|
await this.workspacesInfoCollection.updateOne({ workspace }, update)
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,14 @@
|
|||||||
|
|
||||||
import { LoginInfo, Workspace, WorkspaceLoginInfo } from '@hcengineering/account'
|
import { LoginInfo, Workspace, WorkspaceLoginInfo } from '@hcengineering/account'
|
||||||
import aiBot, { aiBotAccountEmail } from '@hcengineering/ai-bot'
|
import aiBot, { aiBotAccountEmail } from '@hcengineering/ai-bot'
|
||||||
import { AccountRole, systemAccountEmail } from '@hcengineering/core'
|
import { AccountRole, isWorkspaceCreating, MeasureContext, systemAccountEmail } from '@hcengineering/core'
|
||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
|
|
||||||
import config from './config'
|
import config from '../config'
|
||||||
|
import { wait } from './common'
|
||||||
|
|
||||||
|
const ASSIGN_WORKSPACE_DELAY_MS = 5 * 1000 // 5 secs
|
||||||
|
const MAX_ASSIGN_ATTEMPTS = 5
|
||||||
|
|
||||||
export async function assignBotToWorkspace (workspace: string): Promise<Workspace> {
|
export async function assignBotToWorkspace (workspace: string): Promise<Workspace> {
|
||||||
const token = generateToken(systemAccountEmail, { name: '-' }, { service: 'aibot' })
|
const token = generateToken(systemAccountEmail, { name: '-' }, { service: 'aibot' })
|
||||||
@ -102,3 +106,69 @@ export async function getWorkspaceInfo (token: string): Promise<WorkspaceLoginIn
|
|||||||
|
|
||||||
return workspaceInfo.result as WorkspaceLoginInfo
|
return workspaceInfo.result as WorkspaceLoginInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tryGetWorkspaceInfo (ws: string, ctx: MeasureContext): Promise<WorkspaceLoginInfo | undefined> {
|
||||||
|
const systemToken = generateToken(systemAccountEmail, { name: ws })
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
try {
|
||||||
|
const info = await getWorkspaceInfo(systemToken)
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
} catch (e) {
|
||||||
|
ctx.error('Error during get workspace info:', { e })
|
||||||
|
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutByWorkspace = new Map<string, NodeJS.Timeout>()
|
||||||
|
const attemptsByWorkspace = new Map<string, number>()
|
||||||
|
|
||||||
|
export async function tryAssignToWorkspace (
|
||||||
|
workspace: string,
|
||||||
|
ctx: MeasureContext,
|
||||||
|
clearAttempts = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (clearAttempts) {
|
||||||
|
attemptsByWorkspace.delete(workspace)
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutByWorkspace.get(workspace))
|
||||||
|
try {
|
||||||
|
const info = await tryGetWorkspaceInfo(workspace, ctx)
|
||||||
|
|
||||||
|
if (info === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWorkspaceCreating(info?.mode)) {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
void tryAssignToWorkspace(workspace, ctx, false)
|
||||||
|
}, ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
|
|
||||||
|
timeoutByWorkspace.set(workspace, t)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await assignBotToWorkspace(workspace)
|
||||||
|
ctx.info('Assigned to workspace: ', { workspace })
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
ctx.error('Error during assign workspace:', { e })
|
||||||
|
const attempts = attemptsByWorkspace.get(workspace) ?? 0
|
||||||
|
if (attempts < MAX_ASSIGN_ATTEMPTS) {
|
||||||
|
attemptsByWorkspace.set(workspace, attempts + 1)
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
void tryAssignToWorkspace(workspace, ctx, false)
|
||||||
|
}, ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
|
timeoutByWorkspace.set(workspace, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
22
services/ai-bot/pod-ai-bot/src/utils/common.ts
Normal file
22
services/ai-bot/pod-ai-bot/src/utils/common.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
export async function wait (delay: number): Promise<void> {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
}, delay)
|
||||||
|
})
|
||||||
|
}
|
95
services/ai-bot/pod-ai-bot/src/utils/openai.ts
Normal file
95
services/ai-bot/pod-ai-bot/src/utils/openai.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// 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 OpenAI from 'openai'
|
||||||
|
import { countTokens } from '@hcengineering/openai'
|
||||||
|
import { Tiktoken } from 'js-tiktoken'
|
||||||
|
|
||||||
|
import config from '../config'
|
||||||
|
import { HistoryRecord } from '../types'
|
||||||
|
|
||||||
|
export async function translateHtml (client: OpenAI, html: string, lang: string): Promise<string | undefined> {
|
||||||
|
const response = await client.chat.completions.create({
|
||||||
|
model: config.OpenAITranslateModel,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `Your task is to translate the text into ${lang} while preserving the html structure and metadata`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: html
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.choices[0].message.content ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChatCompletion (
|
||||||
|
client: OpenAI,
|
||||||
|
message: OpenAI.ChatCompletionMessageParam,
|
||||||
|
user?: string,
|
||||||
|
history: OpenAI.ChatCompletionMessageParam[] = [],
|
||||||
|
skipCache = true
|
||||||
|
): Promise<OpenAI.ChatCompletion | undefined> {
|
||||||
|
const opt: OpenAI.RequestOptions = {}
|
||||||
|
if (skipCache) {
|
||||||
|
opt.headers = { 'cf-skip-cache': 'true' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await client.chat.completions.create(
|
||||||
|
{
|
||||||
|
messages: [...history, message],
|
||||||
|
model: config.OpenAIModel,
|
||||||
|
user,
|
||||||
|
stream: false
|
||||||
|
},
|
||||||
|
opt
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestSummary (
|
||||||
|
aiClient: OpenAI,
|
||||||
|
encoding: Tiktoken,
|
||||||
|
history: HistoryRecord[]
|
||||||
|
): Promise<{
|
||||||
|
summary?: string
|
||||||
|
tokens: number
|
||||||
|
}> {
|
||||||
|
const summaryPrompt: OpenAI.ChatCompletionMessageParam = {
|
||||||
|
content: `Summarize the following messages, keeping the key points: ${history.map((msg) => `${msg.role}: ${msg.message}`).join('\n')}`,
|
||||||
|
role: 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await createChatCompletion(aiClient, summaryPrompt, undefined, [
|
||||||
|
{ role: 'system', content: 'Make a summary of messages history' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const summary = response?.choices[0].message.content
|
||||||
|
|
||||||
|
if (summary == null) {
|
||||||
|
return { tokens: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = response?.usage?.completion_tokens ?? countTokens([{ content: summary, role: 'assistant' }], encoding)
|
||||||
|
|
||||||
|
return { summary, tokens }
|
||||||
|
}
|
@ -13,28 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import core, { Account, Ref, TxOperations } from '@hcengineering/core'
|
import core, { Account, Client, Ref, TxOperations } from '@hcengineering/core'
|
||||||
|
import { createClient } from '@hcengineering/server-client'
|
||||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||||
import aiBot from '@hcengineering/ai-bot'
|
|
||||||
import { loginBot } from './account'
|
|
||||||
import chunter, { DirectMessage } from '@hcengineering/chunter'
|
import chunter, { DirectMessage } from '@hcengineering/chunter'
|
||||||
|
import aiBot from '@hcengineering/ai-bot'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import { countTokens } from '@hcengineering/openai'
|
|
||||||
import { Tiktoken } from 'js-tiktoken'
|
|
||||||
|
|
||||||
import { HistoryRecord } from './types'
|
export async function connectPlatform (token: string, endpoint: string): Promise<Client> {
|
||||||
import config from './config'
|
return await createClient(endpoint, token)
|
||||||
|
|
||||||
export async function login (): Promise<string | undefined> {
|
|
||||||
const token = (await loginBot())?.token
|
|
||||||
|
|
||||||
if (token !== undefined) {
|
|
||||||
return token
|
|
||||||
} else {
|
|
||||||
return (await loginBot())?.token
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDirect (
|
export async function getDirect (
|
||||||
@ -80,59 +68,3 @@ export async function getDirect (
|
|||||||
|
|
||||||
return dmId
|
return dmId
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createChatCompletion (
|
|
||||||
client: OpenAI,
|
|
||||||
message: OpenAI.ChatCompletionMessageParam,
|
|
||||||
user?: string,
|
|
||||||
history: OpenAI.ChatCompletionMessageParam[] = [],
|
|
||||||
skipCache = true
|
|
||||||
): Promise<OpenAI.ChatCompletion | undefined> {
|
|
||||||
const opt: OpenAI.RequestOptions = {}
|
|
||||||
if (skipCache) {
|
|
||||||
opt.headers = { 'cf-skip-cache': 'true' }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await client.chat.completions.create(
|
|
||||||
{
|
|
||||||
messages: [...history, message],
|
|
||||||
model: config.OpenAIModel,
|
|
||||||
user,
|
|
||||||
stream: false
|
|
||||||
},
|
|
||||||
opt
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestSummary (
|
|
||||||
aiClient: OpenAI,
|
|
||||||
encoding: Tiktoken,
|
|
||||||
history: HistoryRecord[]
|
|
||||||
): Promise<{
|
|
||||||
summary?: string
|
|
||||||
tokens: number
|
|
||||||
}> {
|
|
||||||
const summaryPrompt: OpenAI.ChatCompletionMessageParam = {
|
|
||||||
content: `Summarize the following messages, keeping the key points: ${history.map((msg) => `${msg.role}: ${msg.message}`).join('\n')}`,
|
|
||||||
role: 'user'
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await createChatCompletion(aiClient, summaryPrompt, undefined, [
|
|
||||||
{ role: 'system', content: 'Make a summary of messages history' }
|
|
||||||
])
|
|
||||||
|
|
||||||
const summary = response?.choices[0].message.content
|
|
||||||
|
|
||||||
if (summary == null) {
|
|
||||||
return { tokens: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = response?.usage?.completion_tokens ?? countTokens([{ content: summary, role: 'assistant' }], encoding)
|
|
||||||
|
|
||||||
return { summary, tokens }
|
|
||||||
}
|
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import aiBot, { aiBotAccountEmail, AIBotEvent, AIBotResponseEvent, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
import aiBot, { aiBotAccountEmail, AIMessageEventRequest, AITransferEventRequest } from '@hcengineering/ai-bot'
|
||||||
import chunter, {
|
import chunter, {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
type ChatWidgetTab,
|
type ChatWidgetTab,
|
||||||
@ -43,7 +43,6 @@ import core, {
|
|||||||
Ref,
|
Ref,
|
||||||
Space,
|
Space,
|
||||||
Tx,
|
Tx,
|
||||||
TxCreateDoc,
|
|
||||||
TxOperations,
|
TxOperations,
|
||||||
TxProcessor,
|
TxProcessor,
|
||||||
TxRemoveDoc
|
TxRemoveDoc
|
||||||
@ -60,10 +59,11 @@ import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-
|
|||||||
import workbench, { SidebarEvent, TxSidebarEvent } from '@hcengineering/workbench'
|
import workbench, { SidebarEvent, TxSidebarEvent } from '@hcengineering/workbench'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { AIBotController } from './controller'
|
import { AIControl } from './controller'
|
||||||
import { connectPlatform } from './platform'
|
import { connectPlatform, getDirect } from './utils/platform'
|
||||||
import { HistoryRecord } from './types'
|
import { HistoryRecord } from './types'
|
||||||
import { createChatCompletion, getDirect, login, requestSummary } from './utils'
|
import { loginBot } from './utils/account'
|
||||||
|
import { createChatCompletion, requestSummary } from './utils/openai'
|
||||||
|
|
||||||
const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses
|
const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses
|
||||||
const UPDATE_TYPING_TIMEOUT_MS = 1000
|
const UPDATE_TYPING_TIMEOUT_MS = 1000
|
||||||
@ -95,7 +95,7 @@ export class WorkspaceClient {
|
|||||||
readonly transactorUrl: string,
|
readonly transactorUrl: string,
|
||||||
readonly token: string,
|
readonly token: string,
|
||||||
readonly workspace: string,
|
readonly workspace: string,
|
||||||
readonly controller: AIBotController,
|
readonly controller: AIControl,
|
||||||
readonly ctx: MeasureContext,
|
readonly ctx: MeasureContext,
|
||||||
readonly info: WorkspaceInfoRecord | undefined
|
readonly info: WorkspaceInfoRecord | undefined
|
||||||
) {
|
) {
|
||||||
@ -115,9 +115,6 @@ export class WorkspaceClient {
|
|||||||
await this.uploadAvatarFile(opClient)
|
await this.uploadAvatarFile(opClient)
|
||||||
const typing = await opClient.findAll(chunter.class.TypingInfo, { user: aiBot.account.AIBot })
|
const typing = await opClient.findAll(chunter.class.TypingInfo, { user: aiBot.account.AIBot })
|
||||||
this.typingMap = new Map(typing.map((t) => [t.objectId, t]))
|
this.typingMap = new Map(typing.map((t) => [t.objectId, t]))
|
||||||
const events = await opClient.findAll(aiBot.class.AIBotEvent, {})
|
|
||||||
void this.processEvents(events)
|
|
||||||
|
|
||||||
this.client.notify = (...txes: Tx[]) => {
|
this.client.notify = (...txes: Tx[]) => {
|
||||||
void this.txHandler(opClient, txes)
|
void this.txHandler(opClient, txes)
|
||||||
}
|
}
|
||||||
@ -143,8 +140,6 @@ export class WorkspaceClient {
|
|||||||
await this.blobClient.upload(this.ctx, config.AvatarName, data.length, config.AvatarContentType, data)
|
await this.blobClient.upload(this.ctx, config.AvatarName, data.length, config.AvatarContentType, data)
|
||||||
await this.controller.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified)
|
await this.controller.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified)
|
||||||
this.ctx.info('Avatar file uploaded successfully', { workspace: this.workspace, path: config.AvatarPath })
|
this.ctx.info('Avatar file uploaded successfully', { workspace: this.workspace, path: config.AvatarPath })
|
||||||
} else {
|
|
||||||
this.ctx.info('Avatar file already uploaded', { workspace: this.workspace, path: config.AvatarPath })
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.ctx.error('Failed to upload avatar file', { e })
|
this.ctx.error('Failed to upload avatar file', { e })
|
||||||
@ -155,7 +150,7 @@ export class WorkspaceClient {
|
|||||||
|
|
||||||
private async tryLogin (): Promise<void> {
|
private async tryLogin (): Promise<void> {
|
||||||
this.ctx.info('Logging in: ', { workspace: this.workspace })
|
this.ctx.info('Logging in: ', { workspace: this.workspace })
|
||||||
const token = await login()
|
const token = (await loginBot())?.token
|
||||||
|
|
||||||
clearTimeout(this.loginTimeout)
|
clearTimeout(this.loginTimeout)
|
||||||
|
|
||||||
@ -232,13 +227,13 @@ export class WorkspaceClient {
|
|||||||
|
|
||||||
async createTransferMessage (
|
async createTransferMessage (
|
||||||
client: TxOperations,
|
client: TxOperations,
|
||||||
event: AIBotTransferEvent,
|
event: AITransferEventRequest,
|
||||||
_id: Ref<Doc>,
|
_id: Ref<Doc>,
|
||||||
_class: Ref<Class<Doc>>,
|
_class: Ref<Class<Doc>>,
|
||||||
space: Ref<Space>,
|
space: Ref<Space>,
|
||||||
message: string
|
message: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const op = client.apply(undefined, 'AIBotTransferEvent')
|
const op = client.apply(undefined, 'AITransferEventRequest')
|
||||||
if (event.messageClass === chunter.class.ChatMessage) {
|
if (event.messageClass === chunter.class.ChatMessage) {
|
||||||
await this.startTyping(client, space, _id, _class)
|
await this.startTyping(client, space, _id, _class)
|
||||||
const ref = await op.addCollection<Doc, ChatMessage>(
|
const ref = await op.addCollection<Doc, ChatMessage>(
|
||||||
@ -249,7 +244,7 @@ export class WorkspaceClient {
|
|||||||
event.collection,
|
event.collection,
|
||||||
{ message },
|
{ message },
|
||||||
undefined,
|
undefined,
|
||||||
event.modifiedOn
|
event.createdOn
|
||||||
)
|
)
|
||||||
await op.createMixin(ref, chunter.class.ChatMessage, space, aiBot.mixin.TransferredMessage, {
|
await op.createMixin(ref, chunter.class.ChatMessage, space, aiBot.mixin.TransferredMessage, {
|
||||||
messageId: event.messageId,
|
messageId: event.messageId,
|
||||||
@ -269,7 +264,7 @@ export class WorkspaceClient {
|
|||||||
event.collection,
|
event.collection,
|
||||||
{ message, objectId: parent.attachedTo, objectClass: parent.attachedToClass },
|
{ message, objectId: parent.attachedTo, objectClass: parent.attachedToClass },
|
||||||
undefined,
|
undefined,
|
||||||
event.modifiedOn
|
event.createdOn
|
||||||
)
|
)
|
||||||
await op.createMixin(
|
await op.createMixin(
|
||||||
ref,
|
ref,
|
||||||
@ -436,25 +431,27 @@ export class WorkspaceClient {
|
|||||||
this.historyMap.set(objectId, currentHistory)
|
this.historyMap.set(objectId, currentHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
async processResponseEvent (event: AIBotResponseEvent): Promise<void> {
|
async processMessageEvent (event: AIMessageEventRequest): Promise<void> {
|
||||||
if (this.controller.aiClient === undefined) return
|
if (this.controller.aiClient === undefined) return
|
||||||
const client = await this.opClient
|
|
||||||
|
const { user, objectId, objectClass, messageClass } = event
|
||||||
|
const promptText = markupToText(event.message)
|
||||||
|
const prompt: OpenAI.ChatCompletionMessageParam = { content: promptText, role: 'user' }
|
||||||
|
const promptTokens = countTokens([prompt], this.controller.encoding)
|
||||||
|
|
||||||
if (!this.controller.allowAiReplies(this.workspace, event.email)) {
|
if (!this.controller.allowAiReplies(this.workspace, event.email)) {
|
||||||
await client.remove(event)
|
void this.pushHistory(promptText, 'user', promptTokens, user, objectId, objectClass)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = await this.opClient
|
||||||
|
const op = client.apply(undefined, 'AIMessageRequestEvent')
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
const op = client.apply(undefined, 'AIBotResponseEvent')
|
|
||||||
const { user, objectId, objectClass, messageClass } = event
|
|
||||||
const space = hierarchy.isDerived(objectClass, core.class.Space) ? (objectId as Ref<Space>) : event.objectSpace
|
const space = hierarchy.isDerived(objectClass, core.class.Space) ? (objectId as Ref<Space>) : event.objectSpace
|
||||||
|
|
||||||
await this.startTyping(client, space, objectId, objectClass)
|
await this.startTyping(client, space, objectId, objectClass)
|
||||||
|
|
||||||
const promptText = markupToText(event.message)
|
|
||||||
const prompt: OpenAI.ChatCompletionMessageParam = { content: promptText, role: 'user' }
|
|
||||||
|
|
||||||
const promptTokens = countTokens([prompt], this.controller.encoding)
|
|
||||||
const rawHistory = await this.getHistory(objectId)
|
const rawHistory = await this.getHistory(objectId)
|
||||||
const history = this.toOpenAiHistory(rawHistory, promptTokens)
|
const history = this.toOpenAiHistory(rawHistory, promptTokens)
|
||||||
|
|
||||||
@ -464,10 +461,7 @@ export class WorkspaceClient {
|
|||||||
|
|
||||||
void this.pushHistory(promptText, prompt.role, promptTokens, user, objectId, objectClass)
|
void this.pushHistory(promptText, prompt.role, promptTokens, user, objectId, objectClass)
|
||||||
|
|
||||||
const start = Date.now()
|
|
||||||
const chatCompletion = await createChatCompletion(this.controller.aiClient, prompt, user, history)
|
const chatCompletion = await createChatCompletion(this.controller.aiClient, prompt, user, history)
|
||||||
const end = Date.now()
|
|
||||||
this.ctx.info('Chat completion time: ', { time: end - start })
|
|
||||||
const response = chatCompletion?.choices[0].message.content
|
const response = chatCompletion?.choices[0].message.content
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
@ -509,7 +503,6 @@ export class WorkspaceClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await op.remove(event)
|
|
||||||
await this.finishTyping(op, event.objectId)
|
await this.finishTyping(op, event.objectId)
|
||||||
await op.commit()
|
await op.commit()
|
||||||
await this.controller.transferAIReplyToSupport(parseResponse, {
|
await this.controller.transferAIReplyToSupport(parseResponse, {
|
||||||
@ -523,14 +516,7 @@ export class WorkspaceClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async processTransferEvent (event: AIBotTransferEvent): Promise<void> {
|
async transferToSupport (event: AITransferEventRequest, channelRef?: Ref<OnboardingChannel>): Promise<void> {
|
||||||
const client = await this.opClient
|
|
||||||
|
|
||||||
await this.controller.transfer(event)
|
|
||||||
await client.remove(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
async transferToSupport (event: AIBotTransferEvent, channelRef?: Ref<OnboardingChannel>): Promise<void> {
|
|
||||||
const client = await this.opClient
|
const client = await this.opClient
|
||||||
const key = `${event.toEmail}-${event.fromWorkspace}`
|
const key = `${event.toEmail}-${event.fromWorkspace}`
|
||||||
const channel =
|
const channel =
|
||||||
@ -560,7 +546,7 @@ export class WorkspaceClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async transferToUserDirect (event: AIBotTransferEvent): Promise<void> {
|
async transferToUserDirect (event: AITransferEventRequest): Promise<void> {
|
||||||
const client = await this.opClient
|
const client = await this.opClient
|
||||||
const direct = this.directByEmail.get(event.toEmail) ?? (await getDirect(client, event.toEmail, this.aiAccount))
|
const direct = this.directByEmail.get(event.toEmail) ?? (await getDirect(client, event.toEmail, this.aiAccount))
|
||||||
|
|
||||||
@ -579,7 +565,7 @@ export class WorkspaceClient {
|
|||||||
return this.channelByKey.get(key)
|
return this.channelByKey.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async transfer (event: AIBotTransferEvent): Promise<void> {
|
async transfer (event: AITransferEventRequest): Promise<void> {
|
||||||
if (event.toWorkspace === config.SupportWorkspace) {
|
if (event.toWorkspace === config.SupportWorkspace) {
|
||||||
const channel = this.getChannelRef(event.toEmail, event.fromWorkspace)
|
const channel = this.getChannelRef(event.toEmail, event.fromWorkspace)
|
||||||
|
|
||||||
@ -603,24 +589,6 @@ export class WorkspaceClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processEvents (events: AIBotEvent[]): Promise<void> {
|
|
||||||
if (events.length === 0 || this.opClient === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
try {
|
|
||||||
if (event._class === aiBot.class.AIBotResponseEvent) {
|
|
||||||
void this.processResponseEvent(event as AIBotResponseEvent)
|
|
||||||
} else if (event._class === aiBot.class.AIBotTransferEvent) {
|
|
||||||
void this.processTransferEvent(event as AIBotTransferEvent)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.ctx.error('Error processing event: ', { e })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async close (): Promise<void> {
|
async close (): Promise<void> {
|
||||||
clearTimeout(this.loginTimeout)
|
clearTimeout(this.loginTimeout)
|
||||||
|
|
||||||
@ -639,16 +607,6 @@ export class WorkspaceClient {
|
|||||||
this.ctx.info('Closed workspace client: ', { workspace: this.workspace })
|
this.ctx.info('Closed workspace client: ', { workspace: this.workspace })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCreateTx (tx: TxCreateDoc<Doc>): Promise<void> {
|
|
||||||
if (tx.objectClass === aiBot.class.AIBotResponseEvent) {
|
|
||||||
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AIBotResponseEvent>)
|
|
||||||
await this.processResponseEvent(doc)
|
|
||||||
} else if (tx.objectClass === aiBot.class.AIBotTransferEvent) {
|
|
||||||
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AIBotTransferEvent>)
|
|
||||||
await this.processTransferEvent(doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleRemoveTx (tx: TxRemoveDoc<Doc>): Promise<void> {
|
private async handleRemoveTx (tx: TxRemoveDoc<Doc>): Promise<void> {
|
||||||
if (tx.objectClass === chunter.class.TypingInfo && this.typingMap.has(tx.objectId)) {
|
if (tx.objectClass === chunter.class.TypingInfo && this.typingMap.has(tx.objectId)) {
|
||||||
this.typingMap.delete(tx.objectId)
|
this.typingMap.delete(tx.objectId)
|
||||||
@ -659,9 +617,7 @@ export class WorkspaceClient {
|
|||||||
for (const ttx of txes) {
|
for (const ttx of txes) {
|
||||||
const tx = TxProcessor.extractTx(ttx)
|
const tx = TxProcessor.extractTx(ttx)
|
||||||
|
|
||||||
if (tx._class === core.class.TxCreateDoc) {
|
if (tx._class === core.class.TxRemoveDoc) {
|
||||||
await this.handleCreateTx(tx as TxCreateDoc<Doc>)
|
|
||||||
} else if (tx._class === core.class.TxRemoveDoc) {
|
|
||||||
await this.handleRemoveTx(tx as TxRemoveDoc<Doc>)
|
await this.handleRemoveTx(tx as TxRemoveDoc<Doc>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,62 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
import { Token } from '@hcengineering/server-token'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
||||||
import { IncomingHttpHeaders, type Server } from 'http'
|
import { type Server } from 'http'
|
||||||
import { AnalyticEvent } from '@hcengineering/analytics-collector'
|
import { AnalyticEvent } from '@hcengineering/analytics-collector'
|
||||||
|
import { extractToken } from '@hcengineering/server-client'
|
||||||
|
|
||||||
import { ApiError } from './error'
|
import { ApiError } from './error'
|
||||||
import { Collector } from './collector'
|
import { Collector } from './collector'
|
||||||
import { Action } from './types'
|
import { Action } from './types'
|
||||||
|
|
||||||
const extractCookieToken = (cookie?: string): Token | null => {
|
|
||||||
if (cookie === undefined || cookie === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookies = cookie.split(';')
|
|
||||||
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
|
|
||||||
if (tokenCookie === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedToken = tokenCookie.split('=')[1]
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractAuthorizationToken = (authorization?: string): Token | null => {
|
|
||||||
if (authorization === undefined || authorization === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const encodedToken = authorization.split(' ')[1]
|
|
||||||
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractToken = (headers: IncomingHttpHeaders): Token => {
|
|
||||||
try {
|
|
||||||
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
|
|
||||||
|
|
||||||
if (token === null) {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token
|
|
||||||
} catch {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
||||||
|
|
||||||
const handleRequest = async (
|
const handleRequest = async (
|
||||||
@ -77,8 +32,11 @@ const handleRequest = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
const token = extractToken(req.headers)
|
||||||
|
if (token === undefined) {
|
||||||
|
throw new ApiError(401)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const token = extractToken(req.headers)
|
|
||||||
await fn(req, res, token, next)
|
await fn(req, res, token, next)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
next(err)
|
next(err)
|
||||||
|
@ -13,14 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
import { Token } from '@hcengineering/server-token'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
||||||
import { IncomingHttpHeaders, type Server } from 'http'
|
import { type Server } from 'http'
|
||||||
import { MeasureContext } from '@hcengineering/core'
|
import { MeasureContext } from '@hcengineering/core'
|
||||||
import { Telegraf } from 'telegraf'
|
import { Telegraf } from 'telegraf'
|
||||||
import telegram, { TelegramNotificationRequest } from '@hcengineering/telegram'
|
import telegram, { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||||
import { translate } from '@hcengineering/platform'
|
import { translate } from '@hcengineering/platform'
|
||||||
|
import { extractToken } from '@hcengineering/server-client'
|
||||||
|
|
||||||
import { ApiError } from './error'
|
import { ApiError } from './error'
|
||||||
import { PlatformWorker } from './worker'
|
import { PlatformWorker } from './worker'
|
||||||
@ -29,52 +30,6 @@ import config from './config'
|
|||||||
import { toTelegramHtml, toMediaGroups } from './utils'
|
import { toTelegramHtml, toMediaGroups } from './utils'
|
||||||
import { TgContext } from './telegraf/types'
|
import { TgContext } from './telegraf/types'
|
||||||
|
|
||||||
const extractCookieToken = (cookie?: string): Token | null => {
|
|
||||||
if (cookie === undefined || cookie === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookies = cookie.split(';')
|
|
||||||
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
|
|
||||||
if (tokenCookie === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedToken = tokenCookie.split('=')[1]
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractAuthorizationToken = (authorization?: string): Token | null => {
|
|
||||||
if (authorization === undefined || authorization === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const encodedToken = authorization.split(' ')[1]
|
|
||||||
|
|
||||||
if (encodedToken === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeToken(encodedToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractToken = (headers: IncomingHttpHeaders): Token => {
|
|
||||||
try {
|
|
||||||
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
|
|
||||||
|
|
||||||
if (token === null) {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token
|
|
||||||
} catch {
|
|
||||||
throw new ApiError(401)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
||||||
|
|
||||||
const handleRequest = async (
|
const handleRequest = async (
|
||||||
@ -83,11 +38,14 @@ const handleRequest = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
const token = extractToken(req.headers)
|
||||||
|
if (token === undefined) {
|
||||||
|
throw new ApiError(401)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const token = extractToken(req.headers)
|
|
||||||
await fn(req, res, token, next)
|
await fn(req, res, token, next)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Error during extract token', err)
|
console.error(err)
|
||||||
next(err)
|
next(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user