diff --git a/.vscode/launch.json b/.vscode/launch.json index 46bcee306d6..efb5113e8c0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -566,7 +566,6 @@ "MINIO_SECRET_KEY": "minioadmin", "SERVER_SECRET": "secret", "SERVICE_ID": "analytics-collector", - "SUPPORT_WORKSPACE": "09b65664-b3c3-4ea8-b9a1-c9688bde17f0", "ACCOUNTS_URL": "http://localhost:3000" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], @@ -586,7 +585,6 @@ "MONGO_URL": "mongodb://localhost:27017", "PORT": "4010", "SERVER_SECRET": "secret", - "SUPPORT_WORKSPACE": "09b65664-b3c3-4ea8-b9a1-c9688bde17f0", "FIRST_NAME": "Jolie", "LAST_NAME": "AI", "PASSWORD": "password", diff --git a/desktop-package/package.json b/desktop-package/package.json index 2db83cb964e..f1c03511ffc 100644 --- a/desktop-package/package.json +++ b/desktop-package/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.6.271", + "version": "0.6.435", "main": "dist/main/electron.js", "author": "Hardcore Engineering ", "template": "@hcengineering/default-package", diff --git a/desktop/package.json b/desktop/package.json index 9e1bb126ba7..37c083bd9f6 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/desktop", - "version": "0.6.271", + "version": "0.6.435", "main": "dist/main/electron.js", "template": "@hcengineering/webpack-package", "scripts": { diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 829f0473096..06592146b58 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -246,7 +246,6 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - LAST_NAME_FIRST=true - BRANDING_PATH=/var/cfg/branding.json - - SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0 - AI_BOT_URL=http://host.docker.internal:4010 restart: unless-stopped transactor_cockroach: @@ -282,6 +281,7 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - LAST_NAME_FIRST=true - BRANDING_PATH=/var/cfg/branding.json + - AI_BOT_URL=http://host.docker.internal:4010 restart: unless-stopped green: image: hardcoreeng/green @@ -374,22 +374,21 @@ services: - SERVICE_ID=sign-service - BRANDING_PATH=/var/cfg/branding.json - STATS_URL=http://host.docker.internal:4900 - analytics: - image: hardcoreeng/analytics-collector - extra_hosts: - - 'host.docker.internal:host-gateway' - restart: unless-stopped - ports: - - 4017:4017 - environment: - - SECRET=secret - - PORT=4017 - - MONGO_URL=${MONGO_URL} - - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' - - SERVICE_ID=analytics-collector-service - - ACCOUNTS_URL=http://host.docker.internal:3000 - - SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0 - - STATS_URL=http://host.docker.internal:4900 +# analytics: +# image: hardcoreeng/analytics-collector +# extra_hosts: +# - 'host.docker.internal:host-gateway' +# restart: unless-stopped +# ports: +# - 4017:4017 +# environment: +# - SECRET=secret +# - PORT=4017 +# - MONGO_URL=${MONGO_URL} +# - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' +# - SERVICE_ID=analytics-collector-service +# - ACCOUNTS_URL=http://host.docker.internal:3000 +# - STATS_URL=http://host.docker.internal:4900 aiBot: image: hardcoreeng/ai-bot ports: @@ -401,7 +400,6 @@ services: - SERVER_SECRET=secret - MONGO_URL=${MONGO_URL} - ACCOUNTS_URL=http://host.docker.internal:3000 - - SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0 - STORAGE_CONFIG=${STORAGE_CONFIG} - FIRST_NAME=Jolie - LAST_NAME=AI diff --git a/dev/local-mongo/docker-compose.yaml b/dev/local-mongo/docker-compose.yaml index 8ff0ce8b681..22b38fdf095 100644 --- a/dev/local-mongo/docker-compose.yaml +++ b/dev/local-mongo/docker-compose.yaml @@ -181,23 +181,22 @@ services: resources: limits: memory: 300M - analytics: - image: hardcoreeng/analytics-collector - restart: unless-stopped - ports: - - 4007:4007 - environment: - - SECRET=secret - - PORT=4007 - - MONGO_URL=mongodb://host.docker.internal:27017 - - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' - - SERVICE_ID=analytics-collector-service - - ACCOUNTS_URL=http://account:3000 - - SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0 - deploy: - resources: - limits: - memory: 300M +# analytics: +# image: hardcoreeng/analytics-collector +# restart: unless-stopped +# ports: +# - 4007:4007 +# environment: +# - SECRET=secret +# - PORT=4007 +# - MONGO_URL=mongodb://host.docker.internal:27017 +# - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' +# - SERVICE_ID=analytics-collector-service +# - ACCOUNTS_URL=http://account:3000 +# deploy: +# resources: +# limits: +# memory: 300M aiBot: image: hardcoreeng/ai-bot restart: unless-stopped @@ -205,7 +204,6 @@ services: - SERVER_SECRET=secret - MONGO_URL=mongodb://host.docker.internal:27017 - ACCOUNTS_URL=http://account:3000 - - SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0 - FIRST_NAME=Jolie - LAST_NAME=AI - PASSWORD=password diff --git a/dev/prod/src/analytics.ts b/dev/prod/src/analytics.ts index ac19bdf2d9f..3a040258aa5 100644 --- a/dev/prod/src/analytics.ts +++ b/dev/prod/src/analytics.ts @@ -3,7 +3,6 @@ // import { type AnalyticProvider, Analytics } from "@hcengineering/analytics" -import { AnalyticsCollectorProvider } from './analytics/analyticsCollector' import { PosthogAnalyticProvider } from "./analytics/posthog" import { SentryAnalyticProvider } from "./analytics/sentry" import { type Config } from "./platform" @@ -11,8 +10,7 @@ import { type Config } from "./platform" export function configureAnalytics (config: Config) { const providers: AnalyticProvider[] = [ new SentryAnalyticProvider, - new PosthogAnalyticProvider, - new AnalyticsCollectorProvider + new PosthogAnalyticProvider ] for (const provider of providers) { Analytics.init(provider, config) diff --git a/models/ai-bot/package.json b/models/ai-bot/package.json index 578c28fed50..b0b3eb3d8bf 100644 --- a/models/ai-bot/package.json +++ b/models/ai-bot/package.json @@ -29,16 +29,10 @@ }, "dependencies": { "@hcengineering/ai-bot": "^0.6.0", - "@hcengineering/analytics-collector": "^0.6.0", - "@hcengineering/chunter": "^0.6.20", - "@hcengineering/contact": "^0.6.24", "@hcengineering/core": "^0.6.32", "@hcengineering/model": "^0.6.11", - "@hcengineering/model-contact": "^0.6.1", "@hcengineering/model-core": "^0.6.0", - "@hcengineering/model-view": "^0.6.0", "@hcengineering/platform": "^0.6.11", - "@hcengineering/ui": "^0.6.15", - "@hcengineering/view": "^0.6.13" + "@hcengineering/ui": "^0.6.15" } } diff --git a/models/ai-bot/src/index.ts b/models/ai-bot/src/index.ts index 9d5d87ba5ae..778b243102c 100644 --- a/models/ai-bot/src/index.ts +++ b/models/ai-bot/src/index.ts @@ -14,9 +14,6 @@ // import { type Builder } from '@hcengineering/model' -import core, { type Domain } from '@hcengineering/core' -import chunter from '@hcengineering/chunter' -import analyticsCollector from '@hcengineering/analytics-collector' import aiBot from './plugin' @@ -24,12 +21,4 @@ export { aiBotId } from '@hcengineering/ai-bot' export { aiBotOperation } from './migration' export default aiBot -export const DOMAIN_AI_BOT = 'ai_bot' as Domain - -export function createModel (builder: Builder): void { - builder.createDoc(chunter.class.ChunterExtension, core.space.Model, { - point: 'aside', - ofClass: analyticsCollector.class.OnboardingChannel, - component: aiBot.component.OnboardingChannelPanelExtension - }) -} +export function createModel (builder: Builder): void {} diff --git a/models/ai-bot/src/plugin.ts b/models/ai-bot/src/plugin.ts index 0a0977b16e0..38180ba0d3f 100644 --- a/models/ai-bot/src/plugin.ts +++ b/models/ai-bot/src/plugin.ts @@ -15,10 +15,5 @@ import { mergeIds } from '@hcengineering/platform' import aiBot, { aiBotId } from '@hcengineering/ai-bot' -import type { AnyComponent } from '@hcengineering/ui/src/types' -export default mergeIds(aiBotId, aiBot, { - component: { - OnboardingChannelPanelExtension: '' as AnyComponent - } -}) +export default mergeIds(aiBotId, aiBot, {}) diff --git a/models/analytics-collector/src/plugin.ts b/models/analytics-collector/src/plugin.ts index baf5a23841e..95c3d2a4b76 100644 --- a/models/analytics-collector/src/plugin.ts +++ b/models/analytics-collector/src/plugin.ts @@ -14,8 +14,6 @@ // import { mergeIds } from '@hcengineering/platform' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Ref, Mixin } from '@hcengineering/core' import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector' export default mergeIds(analyticsCollectorId, analyticsCollector, {}) diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 2f3c441015d..5d566620eb8 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -32,13 +32,10 @@ import { TChatMessage, TChatMessageViewlet, TChatSyncInfo, - TChunterExtension, TChunterSpace, TDirectMessage, - TInlineButton, TObjectChatPanel, - TThreadMessage, - TTypingInfo + TThreadMessage } from './types' export { chunterId } from '@hcengineering/chunter' @@ -54,10 +51,7 @@ export function createModel (builder: Builder): void { TThreadMessage, TChatMessageViewlet, TObjectChatPanel, - TChatSyncInfo, - TInlineButton, - TTypingInfo, - TChunterExtension + TChatSyncInfo ) builder.createDoc( @@ -161,10 +155,6 @@ export function createModel (builder: Builder): void { presenter: chunter.component.ThreadMessagePresenter }) - builder.mixin(chunter.class.TypingInfo, core.class.Class, core.mixin.TransientConfiguration, { - broadcastOnly: true - }) - builder.createDoc( view.class.Viewlet, core.space.Model, @@ -315,11 +305,6 @@ export function createModel (builder: Builder): void { defineActions(builder) defineNotifications(builder) - builder.mixin(chunter.class.InlineButton, core.class.Class, core.mixin.IndexConfiguration, { - indexes: [], - searchDisabled: true - }) - builder.mixin(chunter.class.ChatSyncInfo, core.class.Class, core.mixin.IndexConfiguration, { indexes: [], searchDisabled: true diff --git a/models/chunter/src/types.ts b/models/chunter/src/types.ts index 73c7213eecc..78204b02c7c 100644 --- a/models/chunter/src/types.ts +++ b/models/chunter/src/types.ts @@ -24,28 +24,22 @@ import { TypeString, UX } from '@hcengineering/model' -import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core' import type { Channel, ChatMessage, ChatMessageViewlet, ChatSyncInfo, - ChunterExtension, ChunterSpace, DirectMessage, - InlineButton, - InlineButtonAction, ObjectChatPanel, - ThreadMessage, - TypingInfo, - ChunterExtensionPoint + ThreadMessage } from '@hcengineering/chunter' import { type Class, type Doc, type Domain, DOMAIN_MODEL, - DOMAIN_TRANSIENT, IndexKind, type Ref, type Timestamp @@ -54,11 +48,10 @@ import contact, { type ChannelProvider as SocialChannelProvider, type Person } f import activity, { type ActivityMessage } from '@hcengineering/activity' import { TActivityMessage } from '@hcengineering/model-activity' import attachment from '@hcengineering/model-attachment' -import type { IntlString, Resource } from '@hcengineering/platform' +import type { IntlString } from '@hcengineering/platform' import type { DocNotifyContext } from '@hcengineering/notification' import chunter from './plugin' -import type { AnyComponent } from '@hcengineering/ui' export const DOMAIN_CHUNTER = 'chunter' as Domain @@ -94,9 +87,6 @@ export class TChatMessage extends TActivityMessage implements ChatMessage { @Prop(TypeRef(contact.class.ChannelProvider), core.string.Object) provider?: Ref - - @Prop(PropCollection(chunter.class.InlineButton), core.string.Object) - inlineButtons?: number } @Model(chunter.class.ThreadMessage, chunter.class.ChatMessage) @@ -145,26 +135,3 @@ export class TChatSyncInfo extends TDoc implements ChatSyncInfo { hidden!: Ref[] timestamp!: Timestamp } - -@Model(chunter.class.InlineButton, core.class.Doc, DOMAIN_CHUNTER) -export class TInlineButton extends TAttachedDoc implements InlineButton { - name!: string - titleIntl?: IntlString - title?: string - action!: Resource -} - -@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT) -export class TTypingInfo extends TDoc implements TypingInfo { - objectId!: Ref - objectClass!: Ref> - person!: Ref - lastTyping!: Timestamp -} - -@Model(chunter.class.ChunterExtension, core.class.Doc, DOMAIN_MODEL) -export class TChunterExtension extends TDoc implements ChunterExtension { - ofClass!: Ref> - point!: ChunterExtensionPoint - component!: AnyComponent -} diff --git a/models/server-ai-bot/src/index.ts b/models/server-ai-bot/src/index.ts index e9e2940862f..44268a7e82d 100644 --- a/models/server-ai-bot/src/index.ts +++ b/models/server-ai-bot/src/index.ts @@ -13,54 +13,28 @@ // limitations under the License. // -import { type Builder, Mixin } from '@hcengineering/model' -import core, { type Domain, type Ref } from '@hcengineering/core' +import { type Builder } from '@hcengineering/model' +import core from '@hcengineering/core' import serverCore from '@hcengineering/server-core' import serverAiBot from '@hcengineering/server-ai-bot' -import aiBot, { type TransferredMessage } from '@hcengineering/ai-bot' -import chunter, { type ChatMessage } from '@hcengineering/chunter' -import notification from '@hcengineering/notification' -import { TChatMessage } from '@hcengineering/model-chunter' +import chunter from '@hcengineering/chunter' export { serverAiBotId } from '@hcengineering/server-ai-bot' -export const DOMAIN_AI_BOT = 'ai_bot' as Domain -@Mixin(aiBot.mixin.TransferredMessage, chunter.class.ChatMessage) -export class TTransferredMessage extends TChatMessage implements TransferredMessage { - messageId!: Ref - parentMessageId?: Ref -} - export function createModel (builder: Builder): void { - builder.createModel(TTransferredMessage) - - builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverAiBot.trigger.OnMessageSend, - isAsync: true - }) - builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverAiBot.trigger.OnMention, + trigger: serverAiBot.trigger.OnUserStatus, txMatch: { - _class: core.class.TxCreateDoc, - objectClass: notification.class.MentionInboxNotification + objectClass: core.class.UserStatus }, isAsync: true }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverAiBot.trigger.OnMessageNotified, + trigger: serverAiBot.trigger.OnMessageSend, txMatch: { _class: core.class.TxCreateDoc, - objectClass: notification.class.ActivityInboxNotification - }, - isAsync: true - }) - - builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverAiBot.trigger.OnUserStatus, - txMatch: { - objectClass: core.class.UserStatus + objectClass: chunter.class.ChatMessage }, isAsync: true }) diff --git a/plugins/ai-bot-resources/src/components/OnboardingChannelAsideExtension.svelte b/plugins/ai-bot-resources/src/components/OnboardingChannelAsideExtension.svelte deleted file mode 100644 index 7fa63390c82..00000000000 --- a/plugins/ai-bot-resources/src/components/OnboardingChannelAsideExtension.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -
- -
- - diff --git a/plugins/ai-bot-resources/src/index.ts b/plugins/ai-bot-resources/src/index.ts index c43a9bc095a..7a6cbaf4b6f 100644 --- a/plugins/ai-bot-resources/src/index.ts +++ b/plugins/ai-bot-resources/src/index.ts @@ -14,12 +14,7 @@ // import { type Resources } from '@hcengineering/platform' -import OnboardingChannelPanelExtension from './components/OnboardingChannelAsideExtension.svelte' export * from './requests' -export default async (): Promise => ({ - component: { - OnboardingChannelPanelExtension - } -}) +export default async (): Promise => ({}) diff --git a/plugins/ai-bot/src/index.ts b/plugins/ai-bot/src/index.ts index e6799360c4b..a85d3bf0f7a 100644 --- a/plugins/ai-bot/src/index.ts +++ b/plugins/ai-bot/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import { type PersonId, buildSocialIdString, type Mixin, type Ref, SocialIdType, PersonUuid } from '@hcengineering/core' + +import { buildSocialIdString, SocialIdType } from '@hcengineering/core' import type { Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' -import { ChatMessage } from '@hcengineering/chunter' -export * from './types' export * from './rest' export const aiBotId = 'ai-bot' as Plugin @@ -27,22 +26,10 @@ export const aiBotEmailSocialId = buildSocialIdString({ type: SocialIdType.EMAIL, value: aiBotAccountEmail }) -export const aiBotAccount = '5a1a5faa-582c-42a6-8613-fc80a15e3ae8' as PersonUuid - -export interface TransferredMessage extends ChatMessage { - messageId: Ref - parentMessageId?: Ref -} const aiBot = plugin(aiBotId, { metadata: { EndpointURL: '' as Metadata - }, - mixin: { - TransferredMessage: '' as Ref> - }, - account: { - AIBot: '' as PersonId } }) diff --git a/plugins/ai-bot/src/rest.ts b/plugins/ai-bot/src/rest.ts index 61fbdaa33c7..36fc24b41de 100644 --- a/plugins/ai-bot/src/rest.ts +++ b/plugins/ai-bot/src/rest.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -13,40 +13,21 @@ // limitations under the License. // -import { Class, Doc, Markup, PersonId, Ref, Space, Timestamp, type WorkspaceUuid } from '@hcengineering/core' -import { ChatMessage } from '@hcengineering/chunter' +import { Class, Doc, Markup, PersonId, Ref, Space, Timestamp } from '@hcengineering/core' import { Room, RoomLanguage } from '@hcengineering/love' import { Person } from '@hcengineering/contact' - -export enum AIEventType { - Message = 'message', - Transfer = 'transfer' -} +import { ChatMessage } from '@hcengineering/chunter' export interface AIEventRequest { - type: AIEventType - collection: string + message: string messageClass: Ref> messageId: Ref - message: string - createdOn: Timestamp -} - -export interface AIMessageEventRequest extends AIEventRequest { - objectId: Ref objectClass: Ref> + objectId: Ref objectSpace: Ref user: PersonId - email: string -} - -export interface AITransferEventRequest extends AIEventRequest { - toPersonId: PersonId - toWorkspace: WorkspaceUuid - fromWorkspace: WorkspaceUuid - fromWorkspaceName: string - fromWorkspaceUrl: string - parentMessageId?: Ref + collection: string + createdOn: Timestamp } export interface TranslateRequest { diff --git a/plugins/ai-bot/src/types.ts b/plugins/ai-bot/src/types.ts deleted file mode 100644 index c1fd913c639..00000000000 --- a/plugins/ai-bot/src/types.ts +++ /dev/null @@ -1,30 +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 { type WorkspaceUuid, type PersonId } from '@hcengineering/core' - -export enum OnboardingEvent { - OpenChatInSidebar = 'openChatInSidebar' -} - -export interface OpenChatInSidebarData { - personId: PersonId - workspace: WorkspaceUuid -} - -export interface OnboardingEventRequest> { - event: OnboardingEvent - data: T -} diff --git a/plugins/analytics-collector-resources/src/index.ts b/plugins/analytics-collector-resources/src/index.ts index f21bd9da1b0..87ad3705625 100644 --- a/plugins/analytics-collector-resources/src/index.ts +++ b/plugins/analytics-collector-resources/src/index.ts @@ -14,10 +14,5 @@ // import { type Resources } from '@hcengineering/platform' -import { AnalyticsCollectorInlineAction } from './utils' -export default async (): Promise => ({ - function: { - AnalyticsCollectorInlineAction - } -}) +export default async (): Promise => ({}) diff --git a/plugins/analytics-collector-resources/src/utils.ts b/plugins/analytics-collector-resources/src/utils.ts deleted file mode 100644 index 4d90715c6da..00000000000 --- a/plugins/analytics-collector-resources/src/utils.ts +++ /dev/null @@ -1,46 +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 { type InlineButtonAction } from '@hcengineering/chunter' -import analyticsCollector from '@hcengineering/analytics-collector' -import { getMetadata } from '@hcengineering/platform' -import presentation from '@hcengineering/presentation' -import { concatLink } from '@hcengineering/core' - -export const AnalyticsCollectorInlineAction: InlineButtonAction = async ( - button, - messageId, - channelId -): Promise => { - const url = getMetadata(analyticsCollector.metadata.EndpointURL) ?? '' - const token = getMetadata(presentation.metadata.Token) ?? '' - - if (url === '' || token === '') { - return - } - - try { - await fetch(concatLink(url, 'action'), { - method: 'POST', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ _id: button._id, name: button.name, messageId, channelId }) - }) - } catch (e) { - console.error(e) - } -} diff --git a/plugins/analytics-collector/src/index.ts b/plugins/analytics-collector/src/index.ts index 67b4b77a84b..c912d371d6f 100644 --- a/plugins/analytics-collector/src/index.ts +++ b/plugins/analytics-collector/src/index.ts @@ -13,10 +13,10 @@ // limitations under the License. // -import type { IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform' +import type { IntlString, Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import type { Class, Ref } from '@hcengineering/core' -import { Channel, InlineButtonAction } from '@hcengineering/chunter' +import { Channel } from '@hcengineering/chunter' import { OnboardingChannel } from './types' @@ -35,9 +35,6 @@ const analyticsCollector = plugin(analyticsCollectorId, { space: { GeneralOnboardingChannel: '' as Ref }, - function: { - AnalyticsCollectorInlineAction: '' as Resource - }, string: { OnboardingChannelDescription: '' as IntlString, Error: '' as IntlString, diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 515f24f5678..8d8fe3704ca 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -31,7 +31,6 @@ import activity, { type ActivityMessage, type ActivityReference } from '@hcengin import attachment from '@hcengineering/attachment' import { combineActivityMessages, sortActivityMessages } from '@hcengineering/activity-resources' import notification, { type DocNotifyContext } from '@hcengineering/notification' -import chunter from '@hcengineering/chunter' export type LoadMode = 'forward' | 'backward' @@ -309,7 +308,6 @@ export class ChannelDataProvider implements IChannelDataProvider { return { _id: { attachments: attachment.class.Attachment, - inlineButtons: chunter.class.InlineButton, reactions: activity.class.Reaction } } diff --git a/plugins/chunter-resources/src/components/ChunterExtensionComponent.svelte b/plugins/chunter-resources/src/components/ChunterExtensionComponent.svelte deleted file mode 100644 index 29a32ae2074..00000000000 --- a/plugins/chunter-resources/src/components/ChunterExtensionComponent.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - -{#each extensions as extension} - -{/each} diff --git a/plugins/chunter-resources/src/components/InlineButtons.svelte b/plugins/chunter-resources/src/components/InlineButtons.svelte deleted file mode 100644 index 0af2bd8eb2c..00000000000 --- a/plugins/chunter-resources/src/components/InlineButtons.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - -{#each inlineButtons as button} - { - void handleInlineButtonClick(button) - }} - /> -{/each} diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte index 0f04aa37c0c..82886d9c823 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte @@ -23,13 +23,12 @@ import view from '@hcengineering/view' import activity, { ActivityMessage, ActivityMessageViewType, DisplayActivityMessage } from '@hcengineering/activity' import { ActivityDocLink, ActivityMessageTemplate, MessageInlineAction } from '@hcengineering/activity-resources' - import chunter, { ChatMessage, ChatMessageViewlet, InlineButton } from '@hcengineering/chunter' + import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter' import { Attachment } from '@hcengineering/attachment' import { EmptyMarkup } from '@hcengineering/text' import ChatMessageHeader from './ChatMessageHeader.svelte' import ChatMessageInput from './ChatMessageInput.svelte' - import InlineButtons from '../InlineButtons.svelte' import { translatedMessagesStore, translatingMessagesStore, shownTranslatedMessagesStore } from '../../stores' export let value: WithLookup | undefined @@ -162,8 +161,6 @@ let attachments: Attachment[] | undefined = undefined $: attachments = value?.$lookup?.attachments as Attachment[] | undefined - let inlineButtons: InlineButton[] = [] - $: inlineButtons = (value?.$lookup?.inlineButtons ?? []) as InlineButton[] let inlineActions: MessageInlineAction[] = [] @@ -262,21 +259,19 @@
- {#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0} + {#if (value.attachments ?? 0) > 0}
{/if} -
{:else}
- {#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0} + {#if (value.attachments ?? 0) > 0}
{/if} -
{/if} {:else if object} diff --git a/plugins/chunter-resources/src/components/chat/DocAside.svelte b/plugins/chunter-resources/src/components/chat/DocAside.svelte index a18f04fae08..060fe2e233f 100644 --- a/plugins/chunter-resources/src/components/chat/DocAside.svelte +++ b/plugins/chunter-resources/src/components/chat/DocAside.svelte @@ -25,8 +25,6 @@ import { ClassAttributeBar, getDocMixins } from '@hcengineering/view-resources' import { ObjectChatPanel } from '@hcengineering/chunter' - import ChunterExtensionComponent from '../ChunterExtensionComponent.svelte' - export let object: Doc export let objectChatPanel: ObjectChatPanel | undefined @@ -80,6 +78,5 @@ {/if} {/each}
- diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index a1d3e78c9c8..3fddb81e472 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -14,7 +14,7 @@ // import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity' -import type { AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core' +import type { Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core' import { NotificationType } from '@hcengineering/notification' import type { Asset, Plugin, Resource } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform' @@ -58,7 +58,6 @@ export interface ChatMessage extends ActivityMessage { attachments?: number editedOn?: Timestamp provider?: Ref - inlineButtons?: number } /** @@ -84,22 +83,6 @@ export interface ChatSyncInfo extends Doc { timestamp: Timestamp } -export interface TypingInfo extends Doc { - objectId: Ref - objectClass: Ref> - person: Ref - lastTyping: Timestamp -} - -export type InlineButtonAction = (button: InlineButton, message: Ref, channel: Ref) => Promise - -export interface InlineButton extends AttachedDoc { - name: string - titleIntl?: IntlString - title?: string - action: Resource -} - export interface ChatWidgetTab extends WidgetTab { data: { _id?: Ref @@ -111,13 +94,6 @@ export interface ChatWidgetTab extends WidgetTab { } } -export type ChunterExtensionPoint = 'aside' -export interface ChunterExtension extends Doc { - ofClass: Ref> - point: ChunterExtensionPoint - component: AnyComponent -} - /** * @public */ @@ -162,10 +138,7 @@ export default plugin(chunterId, { DirectMessage: '' as Ref>, ChatMessage: '' as Ref>, ChatMessageViewlet: '' as Ref>, - ChatSyncInfo: '' as Ref>, - InlineButton: '' as Ref>, - TypingInfo: '' as Ref>, - ChunterExtension: '' as Ref> + ChatSyncInfo: '' as Ref> }, mixin: { ObjectChatPanel: '' as Ref> diff --git a/plugins/contact/src/utils.ts b/plugins/contact/src/utils.ts index fdc30088930..8e17e2687e9 100644 --- a/plugins/contact/src/utils.ts +++ b/plugins/contact/src/utils.ts @@ -14,15 +14,22 @@ // import { + Account, + AccountRole, AttachedData, buildSocialIdString, Class, Client, Doc, FindResult, + generateId, Hierarchy, + MeasureContext, PersonId, - Ref + Ref, + SocialId, + TxFactory, + Person as GlobalPerson } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import { ColorDefinition } from '@hcengineering/ui' @@ -370,3 +377,127 @@ export async function getAllEmployeesPrimarySocialStrings (client: Client): Prom return Object.values(socialStringsByPerson).map((it) => pickPrimarySocialId(it)) } + +export async function ensureEmployee ( + ctx: MeasureContext, + me: Account, + client: Client, + socialIds: SocialId[], + getGlobalPerson: () => Promise +): Promise | null> { + const txFactory = new TxFactory(me.primarySocialId) + const personByUuid = await client.findOne(contact.class.Person, { personUuid: me.uuid }) + let personRef: Ref | undefined = personByUuid?._id + if (personRef === undefined) { + const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: { $in: me.socialIds } }) + + if (socialIdentity !== undefined && !socialIdentity.confirmed) { + const updateSocialIdentityTx = txFactory.createTxUpdateDoc( + contact.class.SocialIdentity, + contact.space.Contacts, + socialIdentity._id, + { + confirmed: true + } + ) + + await client.tx(updateSocialIdentityTx) + } + + personRef = socialIdentity?.attachedTo + } + + if (personRef === undefined) { + await ctx.with('create-person', {}, async () => { + const globalPerson = await getGlobalPerson() + + if (globalPerson === undefined) { + console.error('Cannot get global person') + return null + } + + const data = { + personUuid: me.uuid, + name: combineName(globalPerson.firstName, globalPerson.lastName), + city: globalPerson.city, + avatarType: AvatarType.COLOR + } + personRef = generateId() + + const createPersonTx = txFactory.createTxCreateDoc(contact.class.Person, contact.space.Contacts, data, personRef) + + await client.tx(createPersonTx) + }) + } else if (personByUuid === undefined) { + const updatePersonTx = txFactory.createTxUpdateDoc(contact.class.Person, contact.space.Contacts, personRef, { + personUuid: me.uuid + }) + + await client.tx(updatePersonTx) + } + + if (me.role !== AccountRole.Guest) { + const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref }) + + if (employee === undefined || !client.getHierarchy().hasMixin(employee, contact.mixin.Employee)) { + await ctx.with('create-employee', {}, async () => { + if (personRef === undefined) { + // something went wrong + console.error('Person not found') + return null + } + + const createEmployeeTx = txFactory.createTxMixin( + personRef, + contact.class.Person, + contact.space.Contacts, + contact.mixin.Employee, + { + active: true + } + ) + + await client.tx(createEmployeeTx) + }) + } + } + + const existingIdentifiers = await client.findAll(contact.class.SocialIdentity, { + attachedTo: personRef, + attachedToClass: contact.class.Person + }) + + for (const socialId of socialIds) { + const existing = existingIdentifiers.find((it) => it.type === socialId.type && it.value === socialId.value) + if (existing === undefined) { + await ctx.with('create-social-identity', {}, async () => { + if (personRef === undefined) { + // something went wrong + console.error('Person not found') + return null + } + + const createSocialIdTx = txFactory.createTxCollectionCUD( + contact.class.Person, + personRef, + contact.space.Contacts, + 'socialIds', + txFactory.createTxCreateDoc(contact.class.SocialIdentity, contact.space.Contacts, { + attachedTo: personRef, + attachedToClass: contact.class.Person, + collection: 'socialIds', + type: socialId.type, + value: socialId.value, + key: buildSocialIdString(socialId), // TODO: fill it in trigger or on DB level as stored calculated column or smth? + confirmed: socialId.verifiedOn !== undefined && socialId.verifiedOn > 0 + }) + ) + + await client.tx(createSocialIdTx) + }) + } + } + + // TODO: check for merged persons with this one and do the merge + return personRef as Ref +} diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 4c64825c997..038a3b8522f 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -113,10 +113,13 @@ export async function getToken ( } function getTokenRoomName (roomName: string, roomId: Ref): string { - const loc = getCurrentLocation() const currentWorkspace = get(currentWorkspaceStore) - return `${currentWorkspace?.url ?? loc.path[1]}_${roomName}_${roomId}` + if (currentWorkspace == null) { + throw new Error('Current workspace not found') + } + + return `${currentWorkspace.uuid}_${roomName}_${roomId}` } export const lk: LKRoom = new LKRoom({ diff --git a/plugins/workbench-resources/src/connect.ts b/plugins/workbench-resources/src/connect.ts index bcae792b6b0..898d479c3ff 100644 --- a/plugins/workbench-resources/src/connect.ts +++ b/plugins/workbench-resources/src/connect.ts @@ -7,27 +7,16 @@ import core, { metricsToString, setCurrentAccount, versionToString, - TxFactory, - generateId, type SocialId, type Account, type Client, - type MeasureContext, type MeasureMetricsContext, type Version, - type Ref, - buildSocialIdString, pickPrimarySocialId, - AccountRole, - type WorkspaceDataId + type WorkspaceDataId, + type Person as GlobalPerson } from '@hcengineering/core' -import contact, { - combineName, - setCurrentEmployee, - AvatarType, - type Person, - type Employee -} from '@hcengineering/contact' +import { setCurrentEmployee, ensureEmployee } from '@hcengineering/contact' import login, { loginId } from '@hcengineering/login' import { broadcastEvent, getMetadata, getResource, OK, setMetadata, translateCB } from '@hcengineering/platform' import presentation, { @@ -364,7 +353,7 @@ export async function connect (title: string): Promise { } // Ensure employee and social identifiers - const employee = await ensureEmployee(ctx, me, newClient, socialIds) + const employee = await ensureEmployee(ctx, me, newClient, socialIds, getGlobalPerson) if (employee == null) { console.log('Failed to ensure employee') @@ -425,129 +414,16 @@ export async function connect (title: string): Promise { return newClient } -async function ensureEmployee ( - ctx: MeasureContext, - me: Account, - client: Client, - socialIds: SocialId[] -): Promise | null> { - const txFactory = new TxFactory(me.primarySocialId) - const personByUuid = await client.findOne(contact.class.Person, { personUuid: me.uuid }) - let personRef: Ref | undefined = personByUuid?._id - if (personRef === undefined) { - const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: { $in: me.socialIds } }) - - if (socialIdentity !== undefined && !socialIdentity.confirmed) { - const updateSocialIdentityTx = txFactory.createTxUpdateDoc( - contact.class.SocialIdentity, - contact.space.Contacts, - socialIdentity._id, - { - confirmed: true - } - ) - - await client.tx(updateSocialIdentityTx) - } - - personRef = socialIdentity?.attachedTo - } - - if (personRef === undefined) { - await ctx.with('create-person', {}, async () => { - const getPerson = await getResource(login.function.GetPerson) - const [status, globalPerson] = await getPerson() - - if (status !== OK) { - console.error('Error getting global person') - return null - } - - const data = { - personUuid: me.uuid, - name: combineName(globalPerson.firstName, globalPerson.lastName), - city: globalPerson.city, - avatarType: AvatarType.COLOR - } - personRef = generateId() - - const createPersonTx = txFactory.createTxCreateDoc(contact.class.Person, contact.space.Contacts, data, personRef) - - await client.tx(createPersonTx) - }) - } else if (personByUuid === undefined) { - const updatePersonTx = txFactory.createTxUpdateDoc(contact.class.Person, contact.space.Contacts, personRef, { - personUuid: me.uuid - }) - - await client.tx(updatePersonTx) - } - - if (me.role !== AccountRole.Guest) { - const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref }) +async function getGlobalPerson (): Promise { + const getPerson = await getResource(login.function.GetPerson) + const [status, globalPerson] = await getPerson() - if (employee === undefined || !client.getHierarchy().hasMixin(employee, contact.mixin.Employee)) { - await ctx.with('create-employee', {}, async () => { - if (personRef === undefined) { - // something went wrong - console.error('Person not found') - return null - } - - const createEmployeeTx = txFactory.createTxMixin( - personRef, - contact.class.Person, - contact.space.Contacts, - contact.mixin.Employee, - { - active: true - } - ) - - await client.tx(createEmployeeTx) - }) - } + if (status !== OK) { + console.error('Error getting global person') + return undefined } - const existingIdentifiers = await client.findAll(contact.class.SocialIdentity, { - attachedTo: personRef, - attachedToClass: contact.class.Person - }) - - for (const socialId of socialIds) { - const existing = existingIdentifiers.find((it) => it.type === socialId.type && it.value === socialId.value) - if (existing === undefined) { - await ctx.with('create-social-identity', {}, async () => { - if (personRef === undefined) { - // something went wrong - console.error('Person not found') - return null - } - - const createSocialIdTx = txFactory.createTxCollectionCUD( - contact.class.Person, - personRef, - contact.space.Contacts, - 'socialIds', - txFactory.createTxCreateDoc(contact.class.SocialIdentity, contact.space.Contacts, { - attachedTo: personRef, - attachedToClass: contact.class.Person, - collection: 'socialIds', - type: socialId.type, - value: socialId.value, - key: buildSocialIdString(socialId), // TODO: fill it in trigger or on DB level as stored calculated column or smth? - confirmed: socialId.verifiedOn !== undefined && socialId.verifiedOn > 0 - }) - ) - - await client.tx(createSocialIdTx) - }) - } - } - - // TODO: check for merged persons with this one and do the merge - - return personRef as Ref + return globalPerson } export function clearMetadata (ws: string): void { diff --git a/pods/server/src/__start.ts b/pods/server/src/__start.ts index 2c0f8de7f5c..a66efa90a77 100644 --- a/pods/server/src/__start.ts +++ b/pods/server/src/__start.ts @@ -73,7 +73,6 @@ setMetadata(serverToken.metadata.Secret, config.serverSecret) setMetadata(serverNotification.metadata.SesUrl, config.sesUrl ?? '') setMetadata(serverNotification.metadata.SesAuthToken, config.sesAuthToken) setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL) -setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE) setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL) const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, { diff --git a/server-plugins/ai-bot-resources/package.json b/server-plugins/ai-bot-resources/package.json index 490e9dd0faa..297fd0abb4f 100644 --- a/server-plugins/ai-bot-resources/package.json +++ b/server-plugins/ai-bot-resources/package.json @@ -46,9 +46,10 @@ "@hcengineering/platform": "^0.6.11", "@hcengineering/server-activity-resources": "^0.6.0", "@hcengineering/server-ai-bot": "^0.6.0", + "@hcengineering/server-contact": "^0.6.1", "@hcengineering/server-core": "^0.6.1", - "@hcengineering/server-token": "^0.6.11", "@hcengineering/server-templates": "^0.6.0", + "@hcengineering/server-token": "^0.6.11", "@hcengineering/templates": "^0.6.11" } } diff --git a/server-plugins/ai-bot-resources/src/index.ts b/server-plugins/ai-bot-resources/src/index.ts index 3e4c27657b5..4182c129388 100644 --- a/server-plugins/ai-bot-resources/src/index.ts +++ b/server-plugins/ai-bot-resources/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -13,367 +13,181 @@ // limitations under the License. // -import { ChatMessage } from '@hcengineering/chunter' -import { AttachedDoc, Tx, TxCreateDoc, TxCUD } from '@hcengineering/core' -import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification' +import core, { + Doc, + PersonUuid, + systemAccountUuid, + Tx, + TxCreateDoc, + TxCUD, + TxProcessor, + TxUpdateDoc, + UserStatus +} from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' +import { getPerson, getPersons } from '@hcengineering/server-contact' +import { aiBotEmailSocialId, AIEventRequest } from '@hcengineering/ai-bot' -// async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise { -// const { members } = direct +import { createAccountRequest, hasAiEndpoint, sendAIEvents } from './utils' +import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter' -// if (!members.includes(aiBot.account.AIBot)) { -// return false -// } - -// const personAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { -// _id: { $in: members as PersonId[] } -// }) -// const persons = new Set(personAccounts.map((account) => account.person)) - -// return persons.size === 2 -// } - -// async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise { -// if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { -// const thread = message as ThreadMessage -// const _id = thread.objectId -// const _class = thread.objectClass - -// return (await control.findAll(control.ctx, _class, { _id }))[0] -// } else { -// const _id = message.attachedTo -// const _class = message.attachedToClass - -// return (await control.findAll(control.ctx, _class, { _id }))[0] -// } -// } - -// function getMessageData (doc: Doc, message: ChatMessage, email: string): AIMessageEventRequest { -// return { -// type: AIEventType.Message, -// createdOn: message.createdOn ?? message.modifiedOn, -// objectId: message.attachedTo, -// objectClass: message.attachedToClass, -// objectSpace: doc.space, -// collection: message.collection, -// messageClass: message._class, -// messageId: message._id, -// message: message.message, -// user: message.createdBy ?? message.modifiedBy, -// email -// } -// } - -// function getThreadMessageData (message: ThreadMessage, email: string): AIMessageEventRequest { -// return { -// type: AIEventType.Message, -// createdOn: message.createdOn ?? message.modifiedOn, -// objectId: message.attachedTo, -// objectClass: message.attachedToClass, -// objectSpace: message.space, -// collection: message.collection, -// messageClass: message._class, -// message: message.message, -// messageId: message._id, -// user: message.createdBy ?? message.modifiedBy, -// email -// } -// } +async function OnUserStatus (txes: TxCUD[], control: TriggerControl): Promise { + if (!hasAiEndpoint()) { + return [] + } -// async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise | undefined> { -// if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) { -// return undefined -// } + if (control.txFactory.account === aiBotEmailSocialId) { + return [] + } -// const parentInfo = ( -// await control.findAll(control.ctx, message.attachedToClass, { -// _id: message.attachedTo as Ref, -// [aiBot.mixin.TransferredMessage]: { $exists: true } -// }) -// )[0] + for (const tx of txes) { + if (![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class)) { + continue + } + + if (tx._class === core.class.TxCreateDoc) { + const createTx = tx as TxCreateDoc + const status = TxProcessor.createDoc2Doc(createTx) + if (status.user === systemAccountUuid) { + continue + } + } + + if (tx._class === core.class.TxUpdateDoc) { + const updateTx = tx as TxUpdateDoc + const val = updateTx.operations.online + if (val !== true) { + continue + } + + const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] + if (status === undefined || status.user === systemAccountUuid) { + continue + } + } + + const aiBotPerson = await getPerson(control, aiBotEmailSocialId) + + if (aiBotPerson === undefined) { + await createAccountRequest(control.workspace.uuid, control.ctx) + return [] + } + } -// if (parentInfo !== undefined) { -// return control.hierarchy.as(parentInfo, aiBot.mixin.TransferredMessage).messageId -// } + return [] +} -// return message.attachedTo as Ref -// } +async function OnMessageSend (originTxs: TxCreateDoc[], control: TriggerControl): Promise { + if (!hasAiEndpoint()) { + return [] + } -// async function createTransferEvent ( -// control: TriggerControl, -// message: ChatMessage, -// account: any, -// data: AIMessageEventRequest -// ): Promise { -// if (account.role !== AccountRole.Owner) { -// return -// } + const { hierarchy } = control + const txes = originTxs.filter((it) => it.modifiedBy !== aiBotEmailSocialId) -// const supportWorkspaceId = getSupportWorkspaceId() + if (txes.length === 0) { + return [] + } -// if (supportWorkspaceId === undefined) { -// return -// } + for (const tx of txes) { + const message = TxProcessor.createDoc2Doc(tx) -// return { -// type: AIEventType.Transfer, -// createdOn: message.createdOn ?? message.modifiedOn, -// messageClass: data.messageClass, -// message: message.message, -// collection: data.collection, -// toWorkspace: supportWorkspaceId, -// toPersonId: account, -// fromWorkspace: control.workspace.uuid, -// // fromWorkspaceName: control.workspace.workspaceName, -// fromWorkspaceUrl: control.workspace.url, -// messageId: message._id, -// parentMessageId: await getThreadParent(control, message) -// } -// } + const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage) + const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise { - // TODO: FIXME - // const account = control.modelDb.findAllSync(contact.class.PersonAccount, { - // _id: (message.createdBy ?? message.modifiedBy) as PersonId - // })[0] - // if (account === undefined) { - // return - // } - // const direct = (await getMessageDoc(message, control)) as DirectMessage - // if (direct === undefined) { - // return - // } - // const isAvailable = await isDirectAvailable(direct, control) - // if (!isAvailable) { - // return - // } - // let messageEvent: AIMessageEventRequest - // if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { - // messageEvent = getThreadMessageData(message as ThreadMessage, account.email) - // } else { - // messageEvent = getMessageData(direct, message, account.email) - // } - // const transferEvent = await createTransferEvent(control, message, account, messageEvent) - // const events = transferEvent !== undefined ? [messageEvent, transferEvent] : [messageEvent] - // await sendAIEvents(events, control.workspace.uuid, control.ctx) -} + if (!hierarchy.isDerived(docClass, chunter.class.DirectMessage)) { + continue + } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function onSupportWorkspaceMessage (control: TriggerControl, message: ChatMessage): Promise { - // TODO: FIXME - // const supportWorkspaceId = getSupportWorkspaceId() - // if (supportWorkspaceId === undefined) { - // return - // } - // if (control.workspace.uuid !== supportWorkspaceId) { - // return - // } - // if (!control.hierarchy.isDerived(message.attachedToClass, analyticsCollector.class.OnboardingChannel)) { - // return - // } - // const channel = (await getMessageDoc(message, control)) as OnboardingChannel - // if (channel === undefined) { - // return - // } - // const { workspaceId, email } = channel - // const account = control.modelDb.findAllSync(contact.class.PersonAccount, { - // _id: (message.createdBy ?? message.modifiedBy) as PersonId - // })[0] - // let data: AIMessageEventRequest - // if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { - // data = getThreadMessageData(message as ThreadMessage, account.email) - // } else { - // data = getMessageData(channel, message, account.email) - // } - // const transferEvent: AITransferEventRequest = { - // type: AIEventType.Transfer, - // createdOn: data.createdOn, - // messageClass: data.messageClass, - // message: message.message, - // collection: data.collection, - // toEmail: email, - // toWorkspace: workspaceId, - // fromWorkspace: control.workspace.uuid, - // fromWorkspaceUrl: control.workspace.url, - // // fromWorkspaceName: control.workspace.workspaceName, - // messageId: message._id, - // parentMessageId: await getThreadParent(control, message) - // } - // await sendAIEvents([transferEvent], control.workspace.uuid, control.ctx) -} - -export async function OnMessageSend (originTxs: TxCUD[], control: TriggerControl): Promise { - // TODO: FIXME - // const { hierarchy } = control - // const txes = originTxs.filter( - // (it) => - // it._class === core.class.TxCreateDoc && - // hierarchy.isDerived(it.objectClass, chunter.class.ChatMessage) && - // !(it.modifiedBy === aiBot.account.AIBot || it.modifiedBy === core.account.System) - // ) - // if (txes.length === 0) { - // return [] - // } - // for (const tx of txes) { - // const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage) - // const message = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - // - // const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass - // - // if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) { - // continue - // } - // - // if (docClass === chunter.class.DirectMessage) { - // await onBotDirectMessageSend(control, message) - // } - // - // if (docClass === analyticsCollector.class.OnboardingChannel) { - // await onSupportWorkspaceMessage(control, message) - // } - // } + if (docClass === chunter.class.DirectMessage) { + await onBotDirectMessageSend(control, message) + } + } return [] } -export async function OnMention (tx: TxCreateDoc[], control: TriggerControl): Promise { - // Note: temporally commented until open ai will be added - // if (tx.objectClass !== notification.class.MentionInboxNotification || tx._class !== core.class.TxCreateDoc) { - // return [] - // } - // - // const mention = TxProcessor.createDoc2Doc(tx) - // - // if (mention.user !== aiBot.account.AIBot) { - // return [] - // } - // - // if (!control.hierarchy.isDerived(mention.mentionedInClass, chunter.class.ChatMessage)) { - // return [] - // } - // - // const message = ( - // await control.findAll(mention.mentionedInClass, { _id: mention.mentionedIn as Ref }) - // )[0] - // - // if (message === undefined) { - // return [] - // } - // - // await createResponseEvent(message, control) - - return [] +function getMessageData (doc: Doc, message: ChatMessage): AIEventRequest { + return { + createdOn: message.createdOn ?? message.modifiedOn, + objectId: message.attachedTo, + objectClass: message.attachedToClass, + objectSpace: doc.space, + collection: message.collection, + messageClass: message._class, + messageId: message._id, + message: message.message, + user: message.createdBy ?? message.modifiedBy + } } -export async function OnMessageNotified ( - tx: TxCreateDoc[], - control: TriggerControl -): Promise { - // Note: temporally commented until open ai will be added - // if (tx.objectClass !== notification.class.ActivityInboxNotification || tx._class !== core.class.TxCreateDoc) { - // return [] - // } - // - // const doc = TxProcessor.createDoc2Doc(tx) - // - // if (doc.user !== aiBot.account.AIBot) { - // return [] - // } - // - // if (!control.hierarchy.isDerived(doc.attachedToClass, chunter.class.ChatMessage)) { - // return [] - // } - // - // const personAccount = await control.modelDb.findOne(contact.class.PersonAccount, { email: aiBotAccountEmail }) - // - // if (personAccount === undefined) { - // return [] - // } - // - // const message = ( - // await control.findAll(doc.attachedToClass, { _id: doc.attachedTo as Ref }) - // )[0] - // - // if (message === undefined) { - // return [] - // } - // - // if (isDocMentioned(personAccount.person, message.message)) { - // return await createResponseEvent(message, control) - // } - // - // if (!control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { - // return [] - // } - // - // const thread = message as ThreadMessage - // TODO: do we really need to find parent??? - // const parent = (await control.findAll(thread.attachedToClass, { _id: thread.attachedTo }))[0] - // - // if (parent === undefined) { - // return [] - // } - // - // if (parent.createdBy === aiBot.account.AIBot) { - // return await createResponseEvent(message, control) - // } - - return [] +function getThreadMessageData (message: ThreadMessage): AIEventRequest { + return { + createdOn: message.createdOn ?? message.modifiedOn, + objectId: message.attachedTo, + objectClass: message.attachedToClass, + objectSpace: message.space, + collection: message.collection, + messageClass: message._class, + message: message.message, + messageId: message._id, + user: message.createdBy ?? message.modifiedBy + } } -export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise { - // TODO: FIXME - return [] // Not implemented - // for (const originTx of txes) { - // const tx = originTx as TxCUD +async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise { + if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { + const thread = message as ThreadMessage + const _id = thread.objectId + const _class = thread.objectClass - // if ( - // tx.objectClass !== core.class.UserStatus || - // ![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class) - // ) { - // continue - // } + return (await control.findAll(control.ctx, _class, { _id }))[0] + } else { + const _id = message.attachedTo + const _class = message.attachedToClass - // if (tx._class === core.class.TxCreateDoc) { - // const createTx = tx as TxCreateDoc - // const status = TxProcessor.createDoc2Doc(createTx) - // if (status.user === aiBot.account.AIBot || status.user === core.account.System || !status.online) { - // continue - // } - // } + return (await control.findAll(control.ctx, _class, { _id }))[0] + } +} - // if (tx._class === core.class.TxUpdateDoc) { - // const updateTx = tx as TxUpdateDoc - // const val = updateTx.operations.online - // if (val !== true) { - // continue - // } +async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise { + const { members } = direct - // const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] - // if (status === undefined || status.user === aiBot.account.AIBot || status.user === core.account.System) { - // continue - // } - // } + if (!members.includes(aiBotEmailSocialId)) { + return false + } - // const account = control.modelDb.findAllSync(contact.class.PersonAccount, { email: aiBotAccountEmail })[0] + const persons = await getPersons(control, members) - // if (account !== undefined) { - // continue - // } + const uuids = new Set( + persons.map((account) => account.personUuid).filter((uuid): uuid is PersonUuid => uuid !== undefined) + ) - // await createAccountRequest(control.workspace, control.ctx) - // } + return uuids.size === 2 +} - // return [] +async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise { + const direct = (await getMessageDoc(message, control)) as DirectMessage + if (direct === undefined) { + return + } + const isAvailable = await isDirectAvailable(direct, control) + if (!isAvailable) { + return + } + let messageEvent: AIEventRequest + if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) { + messageEvent = getThreadMessageData(message as ThreadMessage) + } else { + messageEvent = getMessageData(direct, message) + } + await sendAIEvents([messageEvent], control.workspace.uuid, control.ctx) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { - OnMessageSend, - OnMention, - OnMessageNotified, - OnUserStatus + OnUserStatus, + OnMessageSend } }) diff --git a/server-plugins/ai-bot-resources/src/utils.ts b/server-plugins/ai-bot-resources/src/utils.ts index 9515e91a3e2..cbf93a3c554 100644 --- a/server-plugins/ai-bot-resources/src/utils.ts +++ b/server-plugins/ai-bot-resources/src/utils.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -15,25 +15,11 @@ import { getMetadata } from '@hcengineering/platform' import serverAIBot from '@hcengineering/server-ai-bot' -import { AIEventRequest } from '@hcengineering/ai-bot' import { concatLink, MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' +import { AIEventRequest } from '@hcengineering/ai-bot' -export function getSupportWorkspaceId (): string | undefined { - const supportWorkspaceId = getMetadata(serverAIBot.metadata.SupportWorkspaceId) - - if (supportWorkspaceId === '') { - return undefined - } - - return supportWorkspaceId -} - -export async function sendAIEvents ( - events: AIEventRequest[], - workspace: WorkspaceUuid, - ctx: MeasureContext -): Promise { +export async function createAccountRequest (workspace: WorkspaceUuid, ctx: MeasureContext): Promise { const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? '' if (url === '') { @@ -41,20 +27,26 @@ export async function sendAIEvents ( } try { - await fetch(concatLink(url, '/events'), { + ctx.info('Requesting AI account creation', { url, workspace }) + await fetch(concatLink(url, '/connect'), { method: 'POST', + keepalive: true, headers: { Authorization: 'Bearer ' + generateToken(systemAccountUuid, workspace, { service: 'aibot' }), 'Content-Type': 'application/json' }, - body: JSON.stringify(events) + body: JSON.stringify({}) }) } catch (err) { - ctx.error('Could not send ai events', { err }) + ctx.error('Could not send create ai account request', { err }) } } -export async function createAccountRequest (workspace: WorkspaceUuid, ctx: MeasureContext): Promise { +export async function sendAIEvents ( + events: AIEventRequest[], + workspace: WorkspaceUuid, + ctx: MeasureContext +): Promise { const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? '' if (url === '') { @@ -62,17 +54,20 @@ export async function createAccountRequest (workspace: WorkspaceUuid, ctx: Measu } try { - ctx.info('Requesting AI account creation', { url, workspace }) - await fetch(concatLink(url, '/connect'), { + await fetch(concatLink(url, '/events'), { method: 'POST', - keepalive: true, headers: { Authorization: 'Bearer ' + generateToken(systemAccountUuid, workspace, { service: 'aibot' }), 'Content-Type': 'application/json' }, - body: JSON.stringify({}) + body: JSON.stringify(events) }) } catch (err) { - ctx.error('Could not send create ai account request', { err }) + ctx.error('Could not send ai events', { err }) } } + +export function hasAiEndpoint (): boolean { + const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? '' + return url !== '' +} diff --git a/server-plugins/ai-bot/src/index.ts b/server-plugins/ai-bot/src/index.ts index 8683a1f5f10..03c1361e287 100644 --- a/server-plugins/ai-bot/src/index.ts +++ b/server-plugins/ai-bot/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -22,13 +22,10 @@ export const serverAiBotId = 'server-ai-bot' as Plugin export default plugin(serverAiBotId, { metadata: { - SupportWorkspaceId: '' as Metadata, EndpointURL: '' as Metadata }, trigger: { - OnMessageSend: '' as Resource, - OnMention: '' as Resource, OnUserStatus: '' as Resource, - OnMessageNotified: '' as Resource + OnMessageSend: '' as Resource } }) diff --git a/services/ai-bot/love-agent/src/config.ts b/services/ai-bot/love-agent/src/config.ts index de54401e198..8529d04fb93 100644 --- a/services/ai-bot/love-agent/src/config.ts +++ b/services/ai-bot/love-agent/src/config.ts @@ -17,6 +17,8 @@ import { SttProvider } from './type.js' interface Config { DeepgramApiKey: string + DeepgramModel: string + DeepgramEnModel: string OpenAiModel: string OpenaiApiKey: string OpenaiBaseUrl: string @@ -28,6 +30,8 @@ interface Config { const config: Config = (() => { const params: Partial = { DeepgramApiKey: process.env.DEEPGRAM_API_KEY ?? '', + DeepgramModel: process.env.DEEPGRAM_MODEL ?? 'nova-2-general', + DeepgramEnModel: process.env.DEEPGRAM_EN_MODEL ?? 'nova-3-general', OpenAiModel: process.env.OPENAI_MODEL ?? 'gpt-4o-realtime-preview-2024-12-17', OpenaiApiKey: process.env.OPENAI_API_KEY ?? '', OpenaiBaseUrl: process.env.OPENAI_BASE_URL ?? '', diff --git a/services/ai-bot/love-agent/src/deepgram/stt.ts b/services/ai-bot/love-agent/src/deepgram/stt.ts index 806d29e7bb2..d3393f5ada1 100644 --- a/services/ai-bot/love-agent/src/deepgram/stt.ts +++ b/services/ai-bot/love-agent/src/deepgram/stt.ts @@ -30,7 +30,7 @@ import config from '../config.js' const KEEP_ALIVE_INTERVAL = 10 * 1000 const dgSchema: LiveSchema = { - model: 'nova-2-general', + model: config.DeepgramModel, encoding: 'linear16', smart_format: true, endpointing: 500, @@ -90,18 +90,22 @@ export class STT implements Stt { } subscribe (track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant): void { - if (this.trackBySid.has(publication.sid)) return - this.trackBySid.set(publication.sid, track) - this.participantBySid.set(publication.sid, participant) + const sid = publication.sid + if (sid === undefined) return + if (this.trackBySid.has(sid)) return + this.trackBySid.set(sid, track) + this.participantBySid.set(sid, participant) if (this.isInProgress) { - this.processTrack(publication.sid) + this.processTrack(sid) } } unsubscribe (_: RemoteTrack | undefined, publication: RemoteTrackPublication, participant: RemoteParticipant): void { - this.trackBySid.delete(publication.sid) - this.participantBySid.delete(participant.sid) - this.stopDeepgram(publication.sid) + const sid = publication.sid + if (sid === undefined) return + this.trackBySid.delete(sid) + this.participantBySid.delete(sid) + this.stopDeepgram(sid) } stopDeepgram (sid: string): void { @@ -132,11 +136,13 @@ export class STT implements Stt { if (this.dgConnectionBySid.has(sid)) return const stream = new AudioStream(track) + const language = this.language ?? 'en' const dgConnection = this.deepgram.listen.live({ ...dgSchema, channels: stream.numChannels, sample_rate: stream.sampleRate, - language: this.language ?? 'en' + language, + model: language === 'en' || language === 'en-US' ? config.DeepgramEnModel : config.DeepgramModel }) console.log('Starting deepgram for track', this.room.name, sid) @@ -145,8 +151,8 @@ export class STT implements Stt { }, KEEP_ALIVE_INTERVAL) this.streamBySid.set(sid, stream) - this.dgConnectionBySid.set(track.sid, dgConnection) - this.intervalBySid.set(track.sid, interval) + this.dgConnectionBySid.set(sid, dgConnection) + this.intervalBySid.set(sid, interval) dgConnection.on(LiveTranscriptionEvents.Open, () => { dgConnection.on(LiveTranscriptionEvents.Transcript, (data: LiveTranscriptionEvent) => { @@ -165,8 +171,7 @@ export class STT implements Stt { }) dgConnection.on(LiveTranscriptionEvents.Close, (d) => { - console.log('Connection closed.', d, track.sid) - this.stopDeepgram(track.sid) + this.stopDeepgram(sid) }) dgConnection.on(LiveTranscriptionEvents.Error, (err) => { diff --git a/services/ai-bot/pod-ai-bot/package.json b/services/ai-bot/pod-ai-bot/package.json index 0ba52eed512..9582019ded3 100644 --- a/services/ai-bot/pod-ai-bot/package.json +++ b/services/ai-bot/pod-ai-bot/package.json @@ -55,39 +55,38 @@ }, "dependencies": { "@hcengineering/account": "^0.6.0", + "@hcengineering/account-client": "^0.6.0", "@hcengineering/ai-bot": "^0.6.0", - "@hcengineering/analytics-collector": "^0.6.0", - "@hcengineering/document": "^0.6.0", "@hcengineering/attachment": "^0.6.14", "@hcengineering/chunter": "^0.6.20", "@hcengineering/client": "^0.6.18", "@hcengineering/client-resources": "^0.6.27", "@hcengineering/contact": "^0.6.24", "@hcengineering/core": "^0.6.32", + "@hcengineering/document": "^0.6.0", + "@hcengineering/love": "^0.6.0", "@hcengineering/mongo": "^0.6.1", "@hcengineering/notification": "^0.6.23", "@hcengineering/openai": "^0.6.0", "@hcengineering/platform": "^0.6.11", + "@hcengineering/rank": "^0.6.4", "@hcengineering/server-ai-bot": "^0.6.0", - "@hcengineering/server-analytics-collector-resources": "^0.6.0", "@hcengineering/server-client": "^0.6.0", "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-storage": "^0.6.0", "@hcengineering/server-token": "^0.6.11", "@hcengineering/setting": "^0.6.17", "@hcengineering/text": "^0.6.5", - "@hcengineering/rank": "^0.6.4", - "@hcengineering/server-storage": "^0.6.0", "@hcengineering/workbench": "^0.6.16", - "@hcengineering/love": "^0.6.0", "cors": "^2.8.5", "dotenv": "~16.0.0", "express": "^4.21.2", "fast-equals": "^5.2.2", "form-data": "^4.0.0", "js-tiktoken": "^1.0.14", - "uuid": "^8.3.2", "mongodb": "^6.12.0", "openai": "^4.56.0", + "uuid": "^8.3.2", "ws": "^8.18.0" } } diff --git a/services/ai-bot/pod-ai-bot/src/config.ts b/services/ai-bot/pod-ai-bot/src/config.ts index 4f0b0038186..ea699a49ba9 100644 --- a/services/ai-bot/pod-ai-bot/src/config.ts +++ b/services/ai-bot/pod-ai-bot/src/config.ts @@ -14,7 +14,6 @@ // import OpenAI from 'openai' -import { type WorkspaceUuid } from '@hcengineering/core' interface Config { AccountsURL: string @@ -22,7 +21,6 @@ interface Config { MongoURL: string ServerSecret: string ServiceID: string - SupportWorkspace: WorkspaceUuid FirstName: string LastName: string AvatarPath: string @@ -49,7 +47,6 @@ const config: Config = (() => { MongoURL: process.env.MONGO_URL, ServerSecret: process.env.SERVER_SECRET, ServiceID: process.env.SERVICE_ID ?? 'ai-bot-service', - // SupportWorkspace: process.env.SUPPORT_WORKSPACE as WorkspaceUuid, // TODO: FIXME FirstName: process.env.FIRST_NAME, LastName: process.env.LAST_NAME, AvatarPath: process.env.AVATAR_PATH ?? './assets/avatar.png', diff --git a/services/ai-bot/pod-ai-bot/src/controller.ts b/services/ai-bot/pod-ai-bot/src/controller.ts index 7f2459af2c8..f1f32082db9 100644 --- a/services/ai-bot/pod-ai-bot/src/controller.ts +++ b/services/ai-bot/pod-ai-bot/src/controller.ts @@ -14,22 +14,15 @@ // import { - aiBotAccount, AIEventRequest, - AIEventType, - AIMessageEventRequest, - AITransferEventRequest, ConnectMeetingRequest, DisconnectMeetingRequest, IdentityResponse, - OnboardingEvent, - OnboardingEventRequest, - OpenChatInSidebarData, PostTranscriptRequest, TranslateRequest, TranslateResponse } from '@hcengineering/ai-bot' -import { Markup, MeasureContext, Ref, type WorkspaceDataId, type WorkspaceUuid } from '@hcengineering/core' +import { MeasureContext, PersonUuid, Ref, SocialId, type WorkspaceUuid } from '@hcengineering/core' import { Room } from '@hcengineering/love' import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot' import { getTransactorEndpoint } from '@hcengineering/server-client' @@ -42,11 +35,9 @@ import { StorageAdapter } from '@hcengineering/server-core' import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' import config from './config' import { DbStorage } from './storage' -import { AIReplyTransferData } from './types' -import { tryAssignToWorkspace } from './utils/account' -import { translateHtml } from './utils/openai' -import { SupportWsClient } from './workspace/supportWsClient' import { WorkspaceClient } from './workspace/workspaceClient' +import { translateHtml } from './utils/openai' +import { tryAssignToWorkspace } from './utils/account' const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes @@ -55,36 +46,29 @@ export class AIControl { private readonly closeWorkspaceTimeouts: Map = new Map() private readonly connectingWorkspaces = new Map>() - readonly aiClient?: OpenAI readonly storageAdapter: StorageAdapter - readonly encoding = encodingForModel(config.OpenAIModel) - supportClient: SupportWsClient | undefined = undefined + private readonly openai?: OpenAI + private readonly openaiEncoding = encodingForModel(config.OpenAIModel) constructor ( - readonly storage: DbStorage, + readonly personUuid: PersonUuid, + readonly socialIds: SocialId[], + private readonly storage: DbStorage, private readonly ctx: MeasureContext ) { - this.aiClient = + this.openai = config.OpenAIKey !== '' ? new OpenAI({ apiKey: config.OpenAIKey, baseURL: config.OpenAIBaseUrl === '' ? undefined : config.OpenAIBaseUrl }) : undefined - void this.connectSupportWorkspace() this.storageAdapter = buildStorageFromConfig(storageConfigFromEnv()) } - async getWorkspaceRecord (workspace: string): Promise { - return (await this.storage.getWorkspace(workspace)) ?? { workspace: config.SupportWorkspace } - } - - async connectSupportWorkspace (): Promise { - if (this.supportClient === undefined && config.SupportWorkspace !== '') { - const record = await this.getWorkspaceRecord(config.SupportWorkspace) - this.supportClient = (await this.createWorkspaceClient(config.SupportWorkspace, record)) as SupportWsClient - } + async getWorkspaceRecord (workspace: string): Promise { + return await this.storage.getWorkspace(workspace) } async closeWorkspaceClient (workspace: WorkspaceUuid): Promise { @@ -123,44 +107,31 @@ export class AIControl { const isAssigned = await tryAssignToWorkspace(workspace, this.ctx) if (!isAssigned) { + this.ctx.error('Cannot assign to workspace', { workspace }) return } - const token = generateToken(aiBotAccount, workspace, { service: 'aibot' }) + const token = generateToken(this.personUuid, workspace, { service: 'aibot' }) const endpoint = await getTransactorEndpoint(token) this.ctx.info('Listen workspace: ', { workspace }) - if (workspace === config.SupportWorkspace) { - return new SupportWsClient( - this.storageAdapter, - endpoint, - token, - workspace, - workspace as unknown as WorkspaceDataId, // TODO: FIXME - this, - this.ctx.newChild(workspace, {}), - info - ) - } - return new WorkspaceClient( this.storageAdapter, + this.storage, endpoint, token, workspace, - workspace as unknown as WorkspaceDataId, // TODO: FIXME - this, + this.personUuid, + this.socialIds, this.ctx.newChild(workspace, {}), + this.openai, + this.openaiEncoding, info ) } async initWorkspaceClient (workspace: WorkspaceUuid): Promise { - if (workspace === config.SupportWorkspace) { - return - } - if (this.connectingWorkspaces.has(workspace)) { return await this.connectingWorkspaces.get(workspace) } @@ -168,7 +139,7 @@ export class AIControl { const initPromise = (async () => { try { if (!this.workspaces.has(workspace)) { - const record = await this.getWorkspaceRecord(workspace) + const record = (await this.getWorkspaceRecord(workspace)) ?? { workspace } const client = await this.createWorkspaceClient(workspace, record) if (client === undefined) { return @@ -192,34 +163,6 @@ export class AIControl { await initPromise } - allowAiReplies (workspace: string, email: string): boolean { - if (this.supportClient === undefined) return true - - return this.supportClient.allowAiReplies(workspace, email) - } - - async transferAIReplyToSupport (response: Markup, data: AIReplyTransferData): Promise { - if (this.supportClient === undefined) return - - await this.supportClient.transferAIReply(response, data) - } - - async transfer (event: AITransferEventRequest): Promise { - const workspace = event.toWorkspace - - if (workspace === config.SupportWorkspace) { - if (this.supportClient === undefined) return - - await this.supportClient.transfer(event) - return - } - - const wsClient = await this.getWorkspaceClient(workspace) - if (wsClient === undefined) return - - await wsClient.transfer(event) - } - async close (): Promise { for (const workspace of this.workspaces.values()) { await workspace.close() @@ -230,42 +173,18 @@ export class AIControl { this.workspaces.clear() } - async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise { - 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: WorkspaceUuid): Promise { await this.initWorkspaceClient(workspace) return this.workspaces.get(workspace) } - async openChatInSidebar (data: OpenChatInSidebarData): Promise { - const wsClient = await this.getWorkspaceClient(data.workspace) - if (wsClient === undefined) return - await wsClient.openAIChatInSidebar(data.personId) - } - - async processOnboardingEvent (event: OnboardingEventRequest): Promise { - switch (event.event) { - case OnboardingEvent.OpenChatInSidebar: - await this.openChatInSidebar(event.data as OpenChatInSidebarData) - break - } - } - async translate (req: TranslateRequest): Promise { - if (this.aiClient === undefined) { + if (this.openai === undefined) { return undefined } const html = markupToHTML(req.text) - const result = await translateHtml(this.aiClient, html, req.lang) + const result = await translateHtml(this.openai, html, req.lang) const text = result !== undefined ? htmlToMarkup(result) : req.text return { text, @@ -273,26 +192,13 @@ export class AIControl { } } - async processMessageEvent (workspace: WorkspaceUuid, event: AIMessageEventRequest): Promise { - const wsClient = await this.getWorkspaceClient(workspace) - if (wsClient === undefined) return - - await wsClient.processMessageEvent(event) - } - async processEvent (workspace: WorkspaceUuid, events: AIEventRequest[]): Promise { + if (this.openai === undefined) return + 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 - } + const wsClient = await this.getWorkspaceClient(workspace) + if (wsClient === undefined) continue + await wsClient.processMessageEvent(event) } } @@ -331,14 +237,14 @@ export class AIControl { async processLoveTranscript (request: PostTranscriptRequest): Promise { const parsed = request.roomName.split('_') - const workspace = parsed[0] as WorkspaceUuid - const roomId = parsed[parsed.length - 1] + const workspace = parsed[0] as WorkspaceUuid | undefined + const roomId = parsed[parsed.length - 1] as Ref | undefined - if (workspace === null || roomId === null) return + if (workspace == null || roomId == null) return const wsClient = await this.getWorkspaceClient(workspace) if (wsClient === undefined) return - await wsClient.processLoveTranscript(request.transcript, request.participant, roomId as Ref) + await wsClient.processLoveTranscript(request.transcript, request.participant, roomId) } } diff --git a/services/ai-bot/pod-ai-bot/src/server/server.ts b/services/ai-bot/pod-ai-bot/src/server/server.ts index 329e1137f18..162725970de 100644 --- a/services/ai-bot/pod-ai-bot/src/server/server.ts +++ b/services/ai-bot/pod-ai-bot/src/server/server.ts @@ -12,17 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. // -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Token } from '@hcengineering/server-token' import cors from 'cors' import express, { type Express, type NextFunction, type Request, type Response } from 'express' import { type Server } from 'http' import { TranslateRequest, - OnboardingEventRequest, - AIEventRequest, ConnectMeetingRequest, DisconnectMeetingRequest, + AIEventRequest, PostTranscriptRequest } from '@hcengineering/ai-bot' import { extractToken } from '@hcengineering/server-client' @@ -102,20 +100,18 @@ export function createServer (controller: AIControl, ctx: MeasureContext): Expre app.post( '/love/transcript', wrapRequest(async (req, res, token) => { - // TODO: FIXME - throw new Error('Not implemented') - // if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') { - // throw new ApiError(400) - // } + if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') { + throw new ApiError(400) + } - // if (token.email !== aiBotAccountEmail) { - // throw new ApiError(401) - // } + if (token.account !== controller.personUuid) { + throw new ApiError(401) + } - // await controller.processLoveTranscript(req.body as PostTranscriptRequest) + await controller.processLoveTranscript(req.body as PostTranscriptRequest) - // res.status(200) - // res.json({}) + res.status(200) + res.json({}) }) ) @@ -152,35 +148,19 @@ export function createServer (controller: AIControl, ctx: MeasureContext): Expre app.get( '/love/:roomName/identity', wrapRequest(async (req, res, token) => { - // TODO: FIXME - throw new Error('Not implemented') - // if (token.email !== aiBotAccountEmail) { - // throw new ApiError(401) - // } - - // const roomName = req.params.roomName - // const resp = await controller.getLoveIdentity(roomName) - - // if (resp === undefined) { - // throw new ApiError(404) - // } + if (token.account !== controller.personUuid) { + throw new ApiError(401) + } - // res.status(200) - // res.json(resp) - }) - ) + const roomName = req.params.roomName + const resp = await controller.getLoveIdentity(roomName) - app.post( - '/onboarding', - wrapRequest(async (req, res) => { - if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') { - throw new ApiError(400) + if (resp === undefined) { + throw new ApiError(404) } - await controller.processOnboardingEvent(req.body as OnboardingEventRequest) - res.status(200) - res.json({}) + res.json(resp) }) ) diff --git a/services/ai-bot/pod-ai-bot/src/start.ts b/services/ai-bot/pod-ai-bot/src/start.ts index 87d9dd741cb..e5c8bae01c2 100644 --- a/services/ai-bot/pod-ai-bot/src/start.ts +++ b/services/ai-bot/pod-ai-bot/src/start.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -12,22 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. // -/* eslint-disable @typescript-eslint/no-unused-vars */ import { setMetadata } from '@hcengineering/platform' -import serverAiBot from '@hcengineering/server-ai-bot' -import serverClient from '@hcengineering/server-client' -import serverToken from '@hcengineering/server-token' +import serverClient, { withRetry } from '@hcengineering/server-client' +import serverToken, { generateToken } from '@hcengineering/server-token' import { initStatisticsContext } from '@hcengineering/server-core' -import { AIControl } from './controller' import config from './config' -import { closeDB, DbStorage, getDB } from './storage' +import { getPersonUuid } from './utils/account' import { registerLoaders } from './loaders' +import { getDbStorage } from './storage' +import { AIControl } from './controller' import { createServer, listen } from './server/server' +import type { SocialId } from '@hcengineering/core' +import { getClient as getAccountClient } from '@hcengineering/account-client' export const start = async (): Promise => { setMetadata(serverToken.metadata.Secret, config.ServerSecret) - setMetadata(serverAiBot.metadata.SupportWorkspaceId, config.SupportWorkspace) setMetadata(serverClient.metadata.UserAgent, config.ServiceID) setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) @@ -36,28 +36,29 @@ export const start = async (): Promise => { const ctx = initStatisticsContext('ai-bot-service', {}) ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName }) - const db = await getDB() - const storage = new DbStorage(db) - for (let i = 0; i < 5; i++) { - ctx.info('Creating bot account', { attempt: i }) - try { - // TODO: FIXME replace with signUp with getAccountClient - throw new Error('Not implemented') - // await createAccount(aiBotAccountEmail, config.Password, config.FirstName, config.LastName) - // break - } catch (e) { - ctx.error('Error during account creation', { error: e }) - } - await new Promise((resolve) => setTimeout(resolve, 3000)) + const personUuid = await withRetry( + async () => await getPersonUuid(ctx), + (_, attempt) => attempt >= 5, + 5000 + )() + + if (personUuid === undefined) { + ctx.error('AI Bot Service failed to start. No person found.') + process.exit() } - const aiControl = new AIControl(storage, ctx) + ctx.info('AI person uuid', { personUuid }) + + const storage = await getDbStorage() + const socialIds: SocialId[] = await getAccountClient(config.AccountsURL, generateToken(personUuid)).getSocialIds() + + const aiControl = new AIControl(personUuid, socialIds, storage, ctx) const app = createServer(aiControl, ctx) const server = listen(app, config.Port) const onClose = (): void => { void aiControl.close() - void closeDB() + storage.close() server.close(() => process.exit()) } diff --git a/services/ai-bot/pod-ai-bot/src/storage.ts b/services/ai-bot/pod-ai-bot/src/storage.ts index cd8d462e0e0..18a325d5d5a 100644 --- a/services/ai-bot/pod-ai-bot/src/storage.ts +++ b/services/ai-bot/pod-ai-bot/src/storage.ts @@ -24,7 +24,7 @@ import { HistoryRecord } from './types' const clientRef: MongoClientReference = getMongoClient(config.MongoURL) let client: MongoClient | undefined -export const getDB = (() => { +const connectDB = (() => { return async () => { if (client === undefined) { client = await clientRef.getClient() @@ -34,8 +34,9 @@ export const getDB = (() => { } })() -export const closeDB: () => Promise = async () => { - clientRef.close() +export async function getDbStorage (): Promise { + const db = await connectDB() + return new DbStorage(db) } export class DbStorage { @@ -72,4 +73,8 @@ export class DbStorage { async updateWorkspace (workspace: string, update: UpdateFilter): Promise { await this.workspacesInfoCollection.updateOne({ workspace }, update) } + + close (): void { + clientRef.close() + } } diff --git a/services/ai-bot/pod-ai-bot/src/types.ts b/services/ai-bot/pod-ai-bot/src/types.ts index 946415f8a5d..b2d3399720b 100644 --- a/services/ai-bot/pod-ai-bot/src/types.ts +++ b/services/ai-bot/pod-ai-bot/src/types.ts @@ -14,8 +14,7 @@ // import { ObjectId } from 'mongodb' -import { PersonId, Class, Doc, Ref } from '@hcengineering/core' -import { ChatMessage } from '@hcengineering/chunter' +import { Class, Doc, Ref, PersonUuid } from '@hcengineering/core' export interface HistoryRecord { _id?: ObjectId @@ -24,15 +23,7 @@ export interface HistoryRecord { objectId: Ref objectClass: Ref> role: string - user: PersonId + user: PersonUuid tokens: number timestamp: number } - -export interface AIReplyTransferData { - messageClass: Ref> - email: string - fromWorkspace: string - originalMessageId: Ref - originalParent?: Ref -} diff --git a/services/ai-bot/pod-ai-bot/src/utils/account.ts b/services/ai-bot/pod-ai-bot/src/utils/account.ts index 3d907f07265..b4744b7f7d0 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/account.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/account.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2024-2025 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 @@ -12,63 +12,86 @@ // See the License for the specific language governing permissions and // limitations under the License. // -/* eslint-disable @typescript-eslint/no-unused-vars */ + import { WorkspaceInfoWithStatus, isWorkspaceCreating, - MeasureContext, - systemAccountUuid, type WorkspaceUuid, - AccountRole + AccountRole, + Person as GlobalPerson } from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' -import { getAccountClient } from '@hcengineering/server-client' -import { aiBotAccountEmail } from '@hcengineering/ai-bot' +import { getAccountClient, withRetry } from '@hcengineering/server-client' +import { aiBotAccountEmail, aiBotEmailSocialId } from '@hcengineering/ai-bot' +import { MeasureContext, PersonUuid, systemAccountUuid } from '@hcengineering/core' +import config from '../config' import { wait } from './common' -const ASSIGN_WORKSPACE_DELAY_MS = 5 * 1000 // 5 secs -const MAX_ASSIGN_ATTEMPTS = 5 +const ASSIGN_WORKSPACE_DELAY = 5 * 1000 +const ASSIGN_WORKSPACE_ATTEMPTS = 5 -async function tryGetWorkspaceInfo ( - ws: WorkspaceUuid, - ctx: MeasureContext -): Promise { +const GET_WORKSPACE_DELAY = 3 * 1000 +const GET_WORKSPACE_ATTEMPTS = 3 + +const WAIT_WORKSPACE_CREATION_STEP = 5 * 1000 +const MAX_WAIT_WORKSPACE_CREATION_TIME = 20 * 60 * 1000 + +async function getWorkspaceInfo (ws: WorkspaceUuid, ctx: MeasureContext): Promise { const systemToken = generateToken(systemAccountUuid, ws, { service: 'aibot' }) const accountClient = getAccountClient(systemToken) - for (let i = 0; i < 5; i++) { - try { - const info = await accountClient.getWorkspaceInfo() + return await withRetry( + async () => await accountClient.getWorkspaceInfo(), + (_, attempt) => attempt >= GET_WORKSPACE_ATTEMPTS, + GET_WORKSPACE_DELAY + )() +} - if (info == null) { - await wait(ASSIGN_WORKSPACE_DELAY_MS) - continue - } +async function waitWorkspaceCreation ( + workspace: WorkspaceUuid, + ctx: MeasureContext +): Promise { + let waitedTime = 0 + let attempt = 0 + while (waitedTime < MAX_WAIT_WORKSPACE_CREATION_TIME) { + attempt++ + const info = await getWorkspaceInfo(workspace, ctx) + if (info === undefined) { + ctx.error('Workspace not found', { workspace }) + return undefined + } + + if (isWorkspaceCreating(info?.mode)) { + await waitWorkspaceCreation(workspace, ctx) + const delay = WAIT_WORKSPACE_CREATION_STEP * attempt + waitedTime += delay + if (waitedTime > MAX_WAIT_WORKSPACE_CREATION_TIME) { + ctx.error('Workspace creation timeout', { workspace }) + return undefined + } + await wait(delay) + } else { return info - } catch (e) { - ctx.error('Error during get workspace info:', { e }) - await wait(ASSIGN_WORKSPACE_DELAY_MS) } } } -const timeoutByWorkspace = new Map() -const attemptsByWorkspace = new Map() - -export async function tryAssignToWorkspace ( - workspace: WorkspaceUuid, - ctx: MeasureContext, - clearAttempts = true -): Promise { - if (clearAttempts) { - attemptsByWorkspace.delete(workspace) +export async function getGlobalPerson (token: string): Promise { + try { + const accountClient = getAccountClient(token) + return await accountClient.getPerson() + } catch (err) { + console.error('Error getting global person', err) + return undefined } - clearTimeout(timeoutByWorkspace.get(workspace)) +} + +export async function tryAssignToWorkspace (workspace: WorkspaceUuid, ctx: MeasureContext): Promise { try { const systemToken = generateToken(systemAccountUuid, undefined, { service: 'aibot' }) const accountClient = getAccountClient(systemToken) - const info = await tryGetWorkspaceInfo(workspace, ctx) + const info = await getWorkspaceInfo(workspace, ctx) if (info === undefined) { ctx.error('Workspace not found', { workspace }) @@ -76,29 +99,52 @@ export async function tryAssignToWorkspace ( } if (isWorkspaceCreating(info?.mode)) { - const t = setTimeout(() => { - void tryAssignToWorkspace(workspace, ctx, false) - }, ASSIGN_WORKSPACE_DELAY_MS) - - timeoutByWorkspace.set(workspace, t) - - return false + await waitWorkspaceCreation(workspace, ctx) } - await accountClient.assignWorkspace(aiBotAccountEmail, workspace, AccountRole.User) + await withRetry( + async () => { + await accountClient.assignWorkspace(aiBotAccountEmail, workspace, AccountRole.User) + }, + (_, attempt) => attempt >= ASSIGN_WORKSPACE_ATTEMPTS, + ASSIGN_WORKSPACE_DELAY + )() + 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) - } + ctx.error('Error during assign workspace:', { workspace, e }) } return false } + +async function confirmAccount (uuid: PersonUuid): Promise { + const token = generateToken(uuid, undefined, { service: 'aibot', confirmEmail: aiBotAccountEmail }) + const client = getAccountClient(token) + try { + await client.confirm() + } catch (error: any) { + // ignore + } +} + +export async function getPersonUuid (ctx: MeasureContext): Promise { + const token = generateToken(systemAccountUuid, undefined, { service: 'aibot', confirmEmail: aiBotAccountEmail }) + const accountClient = getAccountClient(token) + const personUuid = await accountClient.findPerson(aiBotEmailSocialId) + + if (personUuid !== undefined) { + await confirmAccount(personUuid) + return personUuid + } + + const result = await accountClient.signUp(aiBotEmailSocialId, config.Password, config.FirstName, config.LastName) + + if (result !== undefined) { + await confirmAccount(result.account) + return result.account + } + + return undefined +} diff --git a/services/ai-bot/pod-ai-bot/src/utils/openai.ts b/services/ai-bot/pod-ai-bot/src/utils/openai.ts index c887b558040..0a291ec1f73 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/openai.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/openai.ts @@ -16,11 +16,12 @@ import { countTokens } from '@hcengineering/openai' import { Tiktoken } from 'js-tiktoken' import OpenAI from 'openai' +import { PersonId } from '@hcengineering/core' + import config from '../config' import { HistoryRecord } from '../types' import { WorkspaceClient } from '../workspace/workspaceClient' import { getTools } from './tools' -import { PersonId } from '@hcengineering/core' export async function translateHtml (client: OpenAI, html: string, lang: string): Promise { const response = await client.chat.completions.create({ @@ -87,26 +88,22 @@ export async function createChatCompletionWithTools ( opt.headers = { 'cf-skip-cache': 'true' } } try { - const res = client.beta.chat.completions - .runTools( - { - messages: [ - { - role: 'system', - content: 'Use tools if possible, don`t use previous information after success using tool for user request' - }, - ...history, - message - ], - model: config.OpenAIModel, - user, - tools: getTools(workspaceClient, user) - }, - opt - ) - .on('message', (message) => { - console.log(message) - }) + const res = client.beta.chat.completions.runTools( + { + messages: [ + { + role: 'system', + content: 'Use tools if possible, don`t use previous information after success using tool for user request' + }, + ...history, + message + ], + model: config.OpenAIModel, + user, + tools: getTools(workspaceClient, user) + }, + opt + ) const str = await res.finalContent() const usage = (await res.totalUsage()).completion_tokens return { diff --git a/services/ai-bot/pod-ai-bot/src/utils/tools.ts b/services/ai-bot/pod-ai-bot/src/utils/tools.ts index 007b3dca8f8..24a02052c17 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/tools.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/tools.ts @@ -1,4 +1,4 @@ -import { MarkupBlobRef, PersonId, Ref } from '@hcengineering/core' +import { MarkupBlobRef, PersonId, Ref, WorkspaceDataId } from '@hcengineering/core' import document, { Document, getFirstRank, Teamspace } from '@hcengineering/document' import { makeRank } from '@hcengineering/rank' import { parseMessageMarkdown } from '@hcengineering/text' @@ -37,12 +37,13 @@ async function pdfToMarkdown ( name: string | undefined ): Promise { if (config.DataLabApiKey !== '') { + const dataId = workspaceClient.workspace as any as WorkspaceDataId try { - const stat = await workspaceClient.storage.stat(workspaceClient.ctx, workspaceClient.wsDataId, fileId) + const stat = await workspaceClient.storage.stat(workspaceClient.ctx, dataId, fileId) if (stat?.contentType !== 'application/pdf') { return } - const file = await workspaceClient.storage.get(workspaceClient.ctx, workspaceClient.wsDataId, fileId) + const file = await workspaceClient.storage.get(workspaceClient.ctx, dataId, fileId) const buffer = await stream2buffer(file) const url = 'https://www.datalab.to/api/v1/marker' @@ -64,7 +65,7 @@ async function pdfToMarkdown ( }) const data = await response.json() - console.log('data', data) + if (data.request_check_url !== undefined) { for (let attempt = 0; attempt < 10; attempt++) { const resp = await fetch(data.request_check_url, { headers }) @@ -95,13 +96,8 @@ async function saveFile ( const client = await workspaceClient.opClient const fileId = uuid() - await workspaceClient.storage.put( - workspaceClient.ctx, - workspaceClient.wsDataId, - fileId, - converted, - 'application/json' - ) + const dataId = workspaceClient.workspace as any as WorkspaceDataId + await workspaceClient.storage.put(workspaceClient.ctx, dataId, fileId, converted, 'application/json') const teamspaces = await client.findAll(document.class.Teamspace, {}) const parent = await client.findOne(document.class.Document, { _id: args.parent as Ref }) diff --git a/services/ai-bot/pod-ai-bot/src/workspace/love.ts b/services/ai-bot/pod-ai-bot/src/workspace/love.ts index 0ca7913acc5..8a1e029a719 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/love.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/love.ts @@ -12,23 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. // -/* eslint-disable @typescript-eslint/no-unused-vars */ import { ConnectMeetingRequest } from '@hcengineering/ai-bot' -import chunter from '@hcengineering/chunter' import core, { - Ref, - TxOperations, concatLink, - TxProcessor, - TxCUD, Doc, + Markup, + MeasureContext, + PersonId, + Ref, + SocialIdType, TxCreateDoc, + TxCUD, + TxOperations, + TxProcessor, TxUpdateDoc, - MeasureContext, - Markup, WorkspaceUuid } from '@hcengineering/core' -import { Person } from '@hcengineering/contact' +import contact, { Person } from '@hcengineering/contact' import love, { getFreeRoomPlace, MeetingMinutes, @@ -39,6 +39,7 @@ import love, { TranscriptionStatus } from '@hcengineering/love' import { jsonToMarkup, MarkupNodeType } from '@hcengineering/text' +import chunter from '@hcengineering/chunter' import config from '../config' @@ -47,6 +48,7 @@ export class LoveController { private participantsInfo: ParticipantInfo[] = [] private rooms: Room[] = [] + private readonly socialIdByPerson = new Map, PersonId>() private readonly meetingMinutes: MeetingMinutes[] = [] constructor ( @@ -171,36 +173,56 @@ export class LoveController { this.connectedRooms.delete(roomId) } + async getSocialId (person: Ref): Promise { + const socialId = + this.socialIdByPerson.get(person) ?? + ( + await this.client.findOne(contact.class.SocialIdentity, { + attachedTo: person, + attachedToClass: contact.class.Person, + type: SocialIdType.HULY + }) + )?.key + + if (socialId === undefined) { + return + } + + this.socialIdByPerson.set(person, socialId) + + return socialId + } + async processTranscript (text: string, person: Ref, roomId: Ref): Promise { - // TODO: FIXME - throw new Error('Not implemented') - // const room = await this.getRoom(roomId) - // const participant = await this.getRoomParticipant(roomId, person) - - // if (room === undefined || participant === undefined) { - // return - // } - - // const personAccount = this.client.getModel().getAccountByPersonId(participant.person)[0] - // const doc = await this.getMeetingMinutes(room) - - // if (doc === undefined) return - // const op = this.client.apply(undefined, undefined, true) - - // await op.addCollection( - // chunter.class.ChatMessage, - // core.space.Workspace, - // doc._id, - // doc._class, - // 'transcription', - // { - // message: this.transcriptToMarkup(text) - // }, - // undefined, - // undefined, - // personAccount._id - // ) - // await op.commit() + const room = await this.getRoom(roomId) + const participant = await this.getRoomParticipant(roomId, person) + + if (room === undefined || participant === undefined) { + return + } + + const socialId = await this.getSocialId(person) + if (socialId === undefined) return + + const doc = await this.getMeetingMinutes(room) + if (doc === undefined) return + + const op = this.client.apply(undefined, undefined, true) + + await op.addCollection( + chunter.class.ChatMessage, + core.space.Workspace, + doc._id, + doc._class, + 'transcription', + { + message: this.transcriptToMarkup(text) + }, + undefined, + undefined, + socialId + ) + await op.commit() } hasActiveConnections (): boolean { @@ -269,7 +291,7 @@ export class LoveController { } } -function getTokenRoomName (workspace: string, roomName: string, roomId: Ref): string { +function getTokenRoomName (workspace: WorkspaceUuid, roomName: string, roomId: Ref): string { return `${workspace}_${roomName}_${roomId}` } diff --git a/services/ai-bot/pod-ai-bot/src/workspace/supportWsClient.ts b/services/ai-bot/pod-ai-bot/src/workspace/supportWsClient.ts deleted file mode 100644 index 1eb97643dc2..00000000000 --- a/services/ai-bot/pod-ai-bot/src/workspace/supportWsClient.ts +++ /dev/null @@ -1,184 +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 core, { Class, Doc, Markup, Ref, Tx, TxCUD, TxOperations, TxUpdateDoc } from '@hcengineering/core' -import { WorkspaceClient } from './workspaceClient' -import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector' -import aiBot from '@hcengineering/ai-bot' -import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter' -import { AIReplyTransferData } from '../types' -import { MarkupMarkType, MarkupNodeType } from '@hcengineering/text' - -interface OnboardingChannelMetadata { - workspace: string - email: string -} - -export class SupportWsClient extends WorkspaceClient { - readonly disableAiRepliesChannels = new Map, OnboardingChannelMetadata>() - readonly disableShowAiRepliesChannels = new Map, OnboardingChannelMetadata>() - readonly assignedChannels = new Set>() - - async initClient (): Promise { - // TODO: FIXME - throw new Error('Not implemented') - // const client = await super.initClient() - - // if (this.client != null) { - // this.client.notify = (...txes) => { - // this.handleTx(client, ...txes) - // } - - // const channels = await client.findAll(analyticsCollector.class.OnboardingChannel, {}) - - // for (const channel of channels) { - // if (channel.members.length > 0) { - // this.assignedChannels.add(channel._id) - // } - // if (channel.disableAIReplies) { - // this.disableAiRepliesChannels.set(channel._id, { workspace: channel.workspaceId, email: channel.email }) - // } - // if (!channel.showAIReplies) { - // this.disableShowAiRepliesChannels.set(channel._id, { workspace: channel.workspaceId, email: channel.email }) - // } - // const key = `${channel.email}-${channel.workspaceId}` - // this.channelByKey.set(key, channel._id) - // } - // } - - // return client - } - - allowAiReplies (workspace: string, email: string): boolean { - for (const [, { workspace: w, email: e }] of this.disableAiRepliesChannels) { - if (w === workspace && e === email) { - return false - } - } - - return true - } - - private handleTx (client: TxOperations, ...txes: Tx[]): void { - void super.txHandler(client, txes as TxCUD[]) - for (const tx of txes) { - switch (tx._class) { - case core.class.TxUpdateDoc: { - this.txUpdateDoc(client, tx as TxUpdateDoc) - break - } - } - } - } - - private async updateChannels (tx: TxUpdateDoc): Promise { - // TODO: FIXME - throw new Error('Not implemented') - // if (this.client === undefined) return - // const updates = tx.operations - - // if (updates.members !== undefined || updates.$push?.members !== undefined) { - // this.assignedChannels.add(tx.objectId) - // } - - // if (updates.disableAIReplies === true) { - // const channel = await this.client.findOne(analyticsCollector.class.OnboardingChannel, { _id: tx.objectId }) - // if (channel === undefined) return - // this.disableAiRepliesChannels.set(channel._id, { workspace: channel.workspaceId, email: channel.email }) - // } else if (updates.disableAIReplies === false) { - // this.disableAiRepliesChannels.delete(tx.objectId) - // } - } - - private txUpdateDoc (client: TxOperations, tx: TxUpdateDoc): void { - const hierarchy = client.getHierarchy() - - if (hierarchy.isDerived(tx.objectClass, analyticsCollector.class.OnboardingChannel)) { - void this.updateChannels(tx as TxUpdateDoc) - } - } - - async transferAIReply (response: Markup, data: AIReplyTransferData): Promise { - const channel = this.getChannelRef(data.email, data.fromWorkspace) - - if (channel === undefined || this.disableShowAiRepliesChannels.has(channel)) return - - const client = await this.opClient - const hierarchy = client.getHierarchy() - - const op = client.apply(undefined, 'AIBotSendAIReplyToSupport') - - const resp = JSON.stringify({ - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { type: MarkupNodeType.text, text: 'AI response:', marks: [{ type: MarkupMarkType.bold, attrs: {} }] } - ] - }, - { - type: 'paragraph', - content: JSON.parse(response).content - } - ] - }) - - if (hierarchy.isDerived(data.messageClass, chunter.class.ThreadMessage) && data.originalParent !== undefined) { - const parent = await this.getThreadParent( - client, - data.originalParent, - channel, - analyticsCollector.class.OnboardingChannel - ) - if (parent !== undefined) { - const ref = await op.addCollection( - chunter.class.ThreadMessage, - parent.space, - parent._id, - parent._class, - 'replies', - { message: resp, objectId: parent.attachedTo, objectClass: parent.attachedToClass } - ) - await op.createMixin( - ref, - chunter.class.ThreadMessage as Ref>, - channel, - aiBot.mixin.TransferredMessage, - { - messageId: data.originalMessageId, - parentMessageId: data.originalParent - } - ) - } - } else { - const ref = await op.addCollection( - chunter.class.ChatMessage, - channel, - channel, - analyticsCollector.class.OnboardingChannel, - 'messages', - { message: resp } - ) - - await op.createMixin(ref, chunter.class.ChatMessage, channel, aiBot.mixin.TransferredMessage, { - messageId: data.originalMessageId, - parentMessageId: data.originalMessageId - }) - } - - await op.commit() - } -} diff --git a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts index ee29f4cadf8..7b10f7ccb71 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts @@ -12,82 +12,70 @@ // See the License for the specific language governing permissions and // limitations under the License. // -/* eslint-disable @typescript-eslint/no-unused-vars */ -import aiBot, { - aiBotAccount, - AIMessageEventRequest, - AITransferEventRequest, +import { + aiBotEmailSocialId, + AIEventRequest, ConnectMeetingRequest, DisconnectMeetingRequest, IdentityResponse } from '@hcengineering/ai-bot' -import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector' import attachment, { Attachment } from '@hcengineering/attachment' -import chunter, { - ChatMessage, - type ChatWidgetTab, - DirectMessage, - ThreadMessage, - TypingInfo -} from '@hcengineering/chunter' -import contact, { AvatarType, combineName, getFirstName, getLastName, getName, Person } from '@hcengineering/contact' +import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter' +import contact, { + AvatarType, + combineName, + ensureEmployee, + getFirstName, + getLastName, + Person +} from '@hcengineering/contact' import core, { - PersonId, + type Account, + AccountRole, Blob, Class, Client, - Data, Doc, - generateId, MeasureContext, + PersonId, + PersonUuid, RateLimiter, Ref, + SocialId, Space, Tx, TxCUD, TxOperations, - TxRemoveDoc, - type WorkspaceUuid, - type WorkspaceDataId + WorkspaceDataId, + type WorkspaceUuid } from '@hcengineering/core' import { Room } from '@hcengineering/love' -import { countTokens } from '@hcengineering/openai' import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot' -import { getOrCreateOnboardingChannel } from '@hcengineering/server-analytics-collector-resources' -import { getAccountClient } from '@hcengineering/server-client' -import { generateToken } from '@hcengineering/server-token' -import { jsonToMarkup, MarkdownParser, markupToText } from '@hcengineering/text' -import workbench, { SidebarEvent, TxSidebarEvent } from '@hcengineering/workbench' import fs from 'fs' import { WithId } from 'mongodb' import OpenAI from 'openai' +import { Tiktoken } from 'js-tiktoken' import { StorageAdapter } from '@hcengineering/server-core' import config from '../config' -import { AIControl } from '../controller' import { HistoryRecord } from '../types' import { createChatCompletionWithTools, requestSummary } from '../utils/openai' -import { connectPlatform, getDirect } from '../utils/platform' +import { connectPlatform } from '../utils/platform' import { LoveController } from './love' - -const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses -const UPDATE_TYPING_TIMEOUT_MS = 1000 +import { DbStorage } from '../storage' +import { jsonToMarkup, MarkdownParser, markupToText } from '@hcengineering/text' +import { countTokens } from '@hcengineering/openai' +import { getAccountClient } from '@hcengineering/server-client' +import { getGlobalPerson } from '../utils/account' export class WorkspaceClient { client: Client | undefined opClient: Promise | TxOperations - loginTimeout: NodeJS.Timeout | undefined - loginDelayMs = 2 * 1000 - - channelByKey = new Map>() rate = new RateLimiter(1) aiPerson: Person | undefined - - typingMap: Map, TypingInfo> = new Map, TypingInfo>() - typingTimeoutsMap: Map, NodeJS.Timeout> = new Map, NodeJS.Timeout>() - directByPersonId = new Map>() + personUuidBySocialId = new Map() historyMap = new Map, WithId[]>() @@ -97,12 +85,15 @@ export class WorkspaceClient { constructor ( readonly storage: StorageAdapter, + readonly dbStorage: DbStorage, readonly transactorUrl: string, readonly token: string, readonly workspace: WorkspaceUuid, - readonly workspaceDataId: WorkspaceDataId | undefined, - readonly controller: AIControl, + readonly personUuid: PersonUuid, + readonly socialIds: SocialId[], readonly ctx: MeasureContext, + readonly openai: OpenAI | undefined, + readonly openaiEncoding: Tiktoken, readonly info: WorkspaceInfoRecord | undefined ) { this.opClient = this.initClient() @@ -111,25 +102,27 @@ export class WorkspaceClient { }) } - get wsDataId (): WorkspaceDataId { - return this.workspaceDataId ?? (this.workspace as unknown as WorkspaceDataId) + private async ensureEmployee (client: Client): Promise { + const me: Account = { + uuid: this.personUuid, + role: AccountRole.User, + primarySocialId: aiBotEmailSocialId, + socialIds: this.socialIds.map((it) => it.key) + } + await ensureEmployee(this.ctx, me, client, this.socialIds, async () => await getGlobalPerson(this.token)) } - protected async initClient (): Promise { - await this.tryLogin() - + private async initClient (): Promise { this.client = await connectPlatform(this.token, this.transactorUrl) - const opClient = new TxOperations(this.client, aiBot.account.AIBot) + const opClient = new TxOperations(this.client, aiBotEmailSocialId) - await this.uploadAvatarFile(opClient) + await this.ensureEmployee(this.client) + await this.checkEmployeeInfo(opClient) if (this.aiPerson !== undefined && config.LoveEndpoint !== '') { - const token = generateToken(aiBotAccount, this.workspace) - this.love = new LoveController(this.workspace, this.ctx.newChild('love', {}), token, opClient, this.aiPerson) + this.love = new LoveController(this.workspace, this.ctx.newChild('love', {}), this.token, opClient, this.aiPerson) } - const typing = await opClient.findAll(chunter.class.TypingInfo, { user: aiBot.account.AIBot }) - this.typingMap = new Map(typing.map((t) => [t.objectId, t])) this.client.notify = (...txes: Tx[]) => { void this.txHandler(opClient, txes as TxCUD[]) } @@ -138,7 +131,7 @@ export class WorkspaceClient { return opClient } - private async uploadAvatarFile (client: TxOperations): Promise { + private async checkEmployeeInfo (client: TxOperations): Promise { this.ctx.info('Upload avatar file', { workspace: this.workspace }) try { @@ -152,8 +145,15 @@ export class WorkspaceClient { if (!isAlreadyUploaded) { const data = fs.readFileSync(config.AvatarPath) - await this.storage.put(this.ctx, this.wsDataId, config.AvatarName, data, config.AvatarContentType, data.length) - await this.controller.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified) + await this.storage.put( + this.ctx, + this.workspace as any as WorkspaceDataId, + config.AvatarName, + data, + config.AvatarContentType, + data.length + ) + await this.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified) this.ctx.info('Avatar file uploaded successfully', { workspace: this.workspace, path: config.AvatarPath }) } } catch (e) { @@ -163,29 +163,21 @@ export class WorkspaceClient { await this.checkPersonData(client) } - private async tryLogin (): Promise { - this.ctx.info('Logging in: ', { workspace: this.workspace }) - const accountClient = getAccountClient() - const token = await accountClient.login(aiBotAccount, config.Password) - - clearTimeout(this.loginTimeout) - - if (token === undefined) { - this.loginTimeout = setTimeout(() => { - if (this.loginDelayMs < MAX_LOGIN_DELAY_MS) { - this.loginDelayMs += 1000 - } - this.ctx.info(`login delay ${this.loginDelayMs} millisecond`) - void this.tryLogin() - }, this.loginDelayMs) + private async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise { + const record = await this.dbStorage.getWorkspace(workspace) + + if (record === undefined) { + await this.dbStorage.addWorkspace({ workspace, avatarPath: path, avatarLastModified: lastModified }) + } else { + await this.dbStorage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } }) } } private async checkPersonData (client: TxOperations): Promise { - this.aiPerson = await client.findOne(contact.class.Person, { personUuid: aiBotAccount }) + this.aiPerson = this.aiPerson ?? (await client.findOne(contact.class.Person, { personUuid: this.personUuid })) if (this.aiPerson === undefined) { - this.ctx.error('Cannot find AI Person ', { personUuid: aiBotAccount }) + this.ctx.error('Cannot find AI Person ', { personUuid: this.personUuid }) return } @@ -202,7 +194,7 @@ export class WorkspaceClient { return } - const exist = await this.storage.stat(this.ctx, this.wsDataId, config.AvatarName) + const exist = await this.storage.stat(this.ctx, this.workspace as any, config.AvatarName) if (exist === undefined) { this.ctx.error('Cannot find file', { file: config.AvatarName, workspace: this.workspace }) @@ -212,148 +204,8 @@ export class WorkspaceClient { await client.diffUpdate(this.aiPerson, { avatar: config.AvatarName as Ref, avatarType: AvatarType.IMAGE }) } - async getThreadParent ( - client: TxOperations, - parentMessageId: Ref, - _id: Ref, - _class: Ref> - ): Promise { - const parent = await client.findOne(chunter.class.ChatMessage, { - attachedTo: _id, - attachedToClass: _class, - [aiBot.mixin.TransferredMessage]: { - messageId: parentMessageId, - parentMessageId: undefined - } - }) - - if (parent !== undefined) { - return parent - } - - return await client.findOne(chunter.class.ChatMessage, { - _id: parentMessageId - }) - } - - async createTransferMessage ( - client: TxOperations, - event: AITransferEventRequest, - _id: Ref, - _class: Ref>, - space: Ref, - message: string - ): Promise { - const op = client.apply(undefined, 'AITransferEventRequest') - if (event.messageClass === chunter.class.ChatMessage) { - await this.startTyping(client, space, _id, _class) - const ref = await op.addCollection( - chunter.class.ChatMessage, - space, - _id, - _class, - event.collection, - { message }, - undefined, - event.createdOn - ) - await op.createMixin(ref, chunter.class.ChatMessage, space, aiBot.mixin.TransferredMessage, { - messageId: event.messageId, - parentMessageId: event.parentMessageId - }) - - await this.finishTyping(client, _id) - } else if (event.messageClass === chunter.class.ThreadMessage && event.parentMessageId !== undefined) { - const parent = await this.getThreadParent(client, event.parentMessageId, _id, _class) - if (parent !== undefined) { - await this.startTyping(client, space, parent._id, parent._class) - const ref = await op.addCollection( - chunter.class.ThreadMessage, - parent.space, - parent._id, - parent._class, - event.collection, - { message, objectId: parent.attachedTo, objectClass: parent.attachedToClass }, - undefined, - event.createdOn - ) - await op.createMixin( - ref, - chunter.class.ThreadMessage as Ref>, - space, - aiBot.mixin.TransferredMessage, - { - messageId: event.messageId, - parentMessageId: event.parentMessageId - } - ) - await this.finishTyping(client, parent._id) - } - } - - await op.commit() - } - - clearTypingTimeout (objectId: Ref): void { - const currentTimeout = this.typingTimeoutsMap.get(objectId) - - if (currentTimeout !== undefined) { - clearTimeout(currentTimeout) - this.typingTimeoutsMap.delete(objectId) - } - } - - async startTyping ( - client: TxOperations, - space: Ref, - objectId: Ref, - objectClass: Ref> - ): Promise { - if (this.aiPerson === undefined) { - return - } - - this.clearTypingTimeout(objectId) - const typingInfo = this.typingMap.get(objectId) - - if (typingInfo === undefined) { - const data: Data = { - objectId, - objectClass, - person: this.aiPerson._id, - lastTyping: Date.now() - } - const _id = await client.createDoc(chunter.class.TypingInfo, space, data) - this.typingMap.set(objectId, { - ...data, - _id, - _class: chunter.class.TypingInfo, - space, - modifiedOn: Date.now(), - modifiedBy: aiBot.account.AIBot - }) - } else { - await client.update(typingInfo, { lastTyping: Date.now() }) - } - - const timeout = setTimeout(() => { - void this.startTyping(client, space, objectId, objectClass) - }, UPDATE_TYPING_TIMEOUT_MS) - this.typingTimeoutsMap.set(objectId, timeout) - } - - async finishTyping (client: TxOperations, objectId: Ref): Promise { - this.clearTypingTimeout(objectId) - const typingInfo = this.typingMap.get(objectId) - - if (typingInfo !== undefined) { - await client.remove(typingInfo) - this.typingMap.delete(objectId) - } - } - // TODO: In feature we also should use embeddings - toOpenAiHistory (history: HistoryRecord[], promptTokens: number): any[] { + private toOpenAiHistory (history: HistoryRecord[], promptTokens: number): any[] { const result: OpenAI.ChatCompletionMessageParam[] = [] let totalTokens = promptTokens @@ -370,29 +222,29 @@ export class WorkspaceClient { return result } - async getHistory (objectId: Ref): Promise[]> { + private async getHistory (objectId: Ref): Promise[]> { if (this.historyMap.has(objectId)) { return this.historyMap.get(objectId) ?? [] } - const historyRecords = await this.controller.storage.getHistoryRecords(this.workspace, objectId) + const historyRecords = await this.dbStorage.getHistoryRecords(this.workspace, objectId) this.historyMap.set(objectId, historyRecords) return historyRecords } - async summarizeHistory ( + private async summarizeHistory ( toSummarize: WithId[], - user: PersonId, + user: PersonUuid, objectId: Ref, objectClass: Ref> ): Promise { - if (this.controller.aiClient === undefined) return + if (this.openai === undefined) return if (this.summarizing.has(objectId)) { return } this.summarizing.add(objectId) - const { summary, tokens } = await requestSummary(this.controller.aiClient, this.controller.encoding, toSummarize) + const { summary, tokens } = await requestSummary(this.openai, this.openaiEncoding, toSummarize) if (summary === undefined) { this.ctx.error('Failed to summarize history', { objectId, objectClass, user }) @@ -411,18 +263,18 @@ export class WorkspaceClient { workspace: this.workspace } - await this.controller.storage.addHistoryRecord(summaryRecord) - await this.controller.storage.removeHistoryRecords(toSummarize.map(({ _id }) => _id)) - const newHistory = await this.controller.storage.getHistoryRecords(this.workspace, objectId) + await this.dbStorage.addHistoryRecord(summaryRecord) + await this.dbStorage.removeHistoryRecords(toSummarize.map(({ _id }) => _id)) + const newHistory = await this.dbStorage.getHistoryRecords(this.workspace, objectId) this.historyMap.set(objectId, newHistory) this.summarizing.delete(objectId) } - async pushHistory ( + private async pushHistory ( message: string, role: 'user' | 'assistant', tokens: number, - user: PersonId, + user: PersonUuid, objectId: Ref, objectClass: Ref> ): Promise { @@ -437,20 +289,29 @@ export class WorkspaceClient { tokens, timestamp: Date.now() } - const _id = await this.controller.storage.addHistoryRecord(newRecord) + const _id = await this.dbStorage.addHistoryRecord(newRecord) currentHistory.push({ ...newRecord, _id }) this.historyMap.set(objectId, currentHistory) } - async getAttachments (client: TxOperations, objectId: Ref): Promise { + private async getAttachments (client: TxOperations, objectId: Ref): Promise { return await client.findAll(attachment.class.Attachment, { attachedTo: objectId }) } - async processMessageEvent (event: AIMessageEventRequest): Promise { - if (this.controller.aiClient === undefined) return + async processMessageEvent (event: AIEventRequest): Promise { + if (this.openai === undefined) return const { user, objectId, objectClass, messageClass } = event const client = await this.opClient + const accountClient = getAccountClient(this.token) + const personUuid = this.personUuidBySocialId.get(user) ?? (await accountClient.findPerson(user)) + + if (personUuid === undefined) { + return + } + + this.personUuidBySocialId.set(user, personUuid) + let promptText = markupToText(event.message) const files = await this.getAttachments(client, event.messageId) if (files.length > 0) { @@ -460,40 +321,32 @@ export class WorkspaceClient { } } const prompt: OpenAI.ChatCompletionMessageParam = { content: promptText, role: 'user' } - const promptTokens = countTokens([prompt], this.controller.encoding) - - if (!this.controller.allowAiReplies(this.workspace, event.email)) { - void this.pushHistory(promptText, 'user', promptTokens, user, objectId, objectClass) - return - } + const promptTokens = countTokens([prompt], this.openaiEncoding) const op = client.apply(undefined, 'AIMessageRequestEvent') const hierarchy = client.getHierarchy() const space = hierarchy.isDerived(objectClass, core.class.Space) ? (objectId as Ref) : event.objectSpace - await this.startTyping(client, space, objectId, objectClass) - const rawHistory = await this.getHistory(objectId) const history = this.toOpenAiHistory(rawHistory, promptTokens) if (history.length < rawHistory.length || history.length > config.MaxHistoryRecords) { - void this.summarizeHistory(rawHistory, user, objectId, objectClass) + void this.summarizeHistory(rawHistory, personUuid, objectId, objectClass) } - void this.pushHistory(promptText, prompt.role, promptTokens, user, objectId, objectClass) + void this.pushHistory(promptText, prompt.role, promptTokens, personUuid, objectId, objectClass) - const chatCompletion = await createChatCompletionWithTools(this, this.controller.aiClient, prompt, user, history) + const chatCompletion = await createChatCompletionWithTools(this, this.openai, prompt, user, history) const response = chatCompletion?.completion if (response == null) { - await this.finishTyping(client, objectId) return } const responseTokens = - chatCompletion?.usage ?? countTokens([{ content: response, role: 'assistant' }], this.controller.encoding) + chatCompletion?.usage ?? countTokens([{ content: response, role: 'assistant' }], this.openaiEncoding) - void this.pushHistory(response, 'assistant', responseTokens, user, objectId, objectClass) + void this.pushHistory(response, 'assistant', responseTokens, personUuid, objectId, objectClass) const parser = new MarkdownParser([], '', '') const parseResponse = jsonToMarkup(parser.parse(response)) @@ -523,101 +376,10 @@ export class WorkspaceClient { ) } } - - await this.finishTyping(op, event.objectId) await op.commit() - await this.controller.transferAIReplyToSupport(parseResponse, { - messageClass, - email: event.email, - fromWorkspace: this.workspace, - originalMessageId: event.messageId, - originalParent: hierarchy.isDerived(event.objectClass, chunter.class.ChatMessage) - ? (event.objectId as Ref) - : undefined - }) - } - - async transferToSupport (event: AITransferEventRequest, channelRef?: Ref): Promise { - // TODO: FIXME - throw new Error('Not implemented') - // const client = await this.opClient - // const key = `${event.toEmail}-${event.fromWorkspace}` - // const channel = - // channelRef ?? - // this.channelByKey.get(key) ?? - // ( - // await getOrCreateOnboardingChannel(this.ctx, client, event.toEmail, { - // workspaceId: event.fromWorkspace, - // workspaceName: event.fromWorkspaceName, - // workspaceUrl: event.fromWorkspaceUrl - // }) - // )[0] - - // if (channel === undefined) { - // return - // } - - // this.channelByKey.set(key, channel) - - // await this.createTransferMessage( - // client, - // event, - // channel, - // analyticsCollector.class.OnboardingChannel, - // channel, - // event.message - // ) - } - - async transferToUserDirect (event: AITransferEventRequest): Promise { - const client = await this.opClient - const direct = - this.directByPersonId.get(event.toPersonId) ?? (await getDirect(client, event.toPersonId, this.aiPerson?._id)) - - if (direct === undefined) { - return - } - - this.directByPersonId.set(event.toPersonId, direct) - - await this.createTransferMessage(client, event, direct, chunter.class.DirectMessage, direct, event.message) - } - - getChannelRef (email: string, workspace: string): Ref | undefined { - const key = `${email}-${workspace}` - - return this.channelByKey.get(key) - } - - async transfer (event: AITransferEventRequest): Promise { - // TODO: FIXME - throw new Error('Not implemented') - // if (event.toWorkspace === config.SupportWorkspace) { - // const channel = this.getChannelRef(event.toEmail, event.fromWorkspace) - - // if (channel !== undefined) { - // await this.transferToSupport(event, channel) - // } else { - // // If we dont have OnboardingChannel we should call it sync to prevent multiple channel for the same user and workspace - // await this.rate.add(async () => { - // await this.transferToSupport(event) - // }) - // } - // } else { - // if (this.directByPersonId.has(event.toPersonId)) { - // await this.transferToUserDirect(event) - // } else { - // // If we dont have Direct with user we should call it sync to prevent multiple directs for the same user - // await this.rate.add(async () => { - // await this.transferToUserDirect(event) - // }) - // } - // } } async close (): Promise { - clearTimeout(this.loginTimeout) - if (this.client !== undefined) { await this.client.close() } @@ -633,73 +395,16 @@ export class WorkspaceClient { this.ctx.info('Closed workspace client: ', { workspace: this.workspace }) } - private async handleRemoveTx (tx: TxRemoveDoc): Promise { - if (tx.objectClass === chunter.class.TypingInfo && this.typingMap.has(tx.objectId)) { - this.typingMap.delete(tx.objectId) - } - } - - protected async txHandler (_: TxOperations, txes: TxCUD[]): Promise { + private async txHandler (_: TxOperations, txes: TxCUD[]): Promise { if (this.love !== undefined) { this.love.txHandler(txes) } - - for (const tx of txes) { - if (tx._class === core.class.TxRemoveDoc) { - await this.handleRemoveTx(tx as TxRemoveDoc) - } - } - } - - async openAIChatInSidebar (personId: PersonId): Promise { - const client = await this.opClient - const direct = this.directByPersonId.get(personId) ?? (await getDirect(client, personId, this.aiPerson?._id)) - - if (direct === undefined || this.aiPerson === undefined) { - return - } - - this.directByPersonId.set(personId, direct) - - const hierarchy = client.getHierarchy() - const name = getName(hierarchy, this.aiPerson) - - const tab: ChatWidgetTab = { - id: `chunter_${direct}`, - name, - iconComponent: chunter.component.DirectIcon, - iconProps: { - _id: direct, - size: 'tiny' - }, - data: { - _id: direct, - _class: chunter.class.DirectMessage, - channelName: name - } - } - - const tx: TxSidebarEvent = { - _id: generateId(), - _class: workbench.class.TxSidebarEvent, - objectSpace: core.space.DerivedTx, - space: core.space.DerivedTx, - event: SidebarEvent.OpenWidget, - params: { - widget: chunter.ids.ChatWidget, - tab - }, - modifiedOn: Date.now(), - modifiedBy: aiBot.account.AIBot - } - - await client.tx(tx) } async loveConnect (request: ConnectMeetingRequest): Promise { await this.opClient if (this.love === undefined) { - console.error('Love is not initialized') + this.ctx.error('Love controller is not initialized') return } await this.love.connect(request) diff --git a/services/analytics-collector/pod-analytics-collector/src/collector.ts b/services/analytics-collector/pod-analytics-collector/src/collector.ts index 433170098f2..decda9b8ce9 100644 --- a/services/analytics-collector/pod-analytics-collector/src/collector.ts +++ b/services/analytics-collector/pod-analytics-collector/src/collector.ts @@ -22,7 +22,7 @@ import { Db, Collection } from 'mongodb' import { WorkspaceClient } from './workspaceClient' import config from './config' import { SupportWsClient } from './supportWsClient' -import { Action, OnboardingMessage } from './types' +import { OnboardingMessage } from './types' const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes @@ -194,24 +194,6 @@ export class Collector { await client.pushEvents(events, token.workspace, person, this.onboardingMessagesCollection) } - async processAction (action: Action, token: Token): Promise { - const ws = token.workspace - - if (ws !== config.SupportWorkspace) { - return - } - - const person = await this.getPerson(token.account, token.workspace) - - if (person === undefined) { - return - } - - const client = this.getSupportWorkspaceClient() - - await client.processAction(action, person, this.onboardingMessagesCollection) - } - async close (): Promise { for (const [, client] of this.workspaces) { await client.close() diff --git a/services/analytics-collector/pod-analytics-collector/src/config.ts b/services/analytics-collector/pod-analytics-collector/src/config.ts index bb8aed909c9..99d16feeb27 100644 --- a/services/analytics-collector/pod-analytics-collector/src/config.ts +++ b/services/analytics-collector/pod-analytics-collector/src/config.ts @@ -33,7 +33,6 @@ const config: Config = (() => { MongoDb: process.env.MONGO_DB ?? '%analytics-collector', Secret: process.env.SECRET, ServiceID: process.env.SERVICE_ID ?? 'analytics-collector-service', - SupportWorkspace: process.env.SUPPORT_WORKSPACE, AccountsUrl: process.env.ACCOUNTS_URL, SentryDSN: process.env.SENTRY_DSN ?? '' } diff --git a/services/analytics-collector/pod-analytics-collector/src/main.ts b/services/analytics-collector/pod-analytics-collector/src/main.ts index 8d7d1deb6f0..f2a2acca001 100644 --- a/services/analytics-collector/pod-analytics-collector/src/main.ts +++ b/services/analytics-collector/pod-analytics-collector/src/main.ts @@ -13,19 +13,12 @@ // limitations under the License. // -import { setMetadata } from '@hcengineering/platform' -import serverToken from '@hcengineering/server-token' import { Analytics } from '@hcengineering/analytics' import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service' -import serverClient from '@hcengineering/server-client' import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import { join } from 'path' import config from './config' -import { createServer, listen } from './server' -import { Collector } from './collector' -import { registerLoaders } from './loaders' -import { closeDB, getDB } from './storage' import { initStatisticsContext } from '@hcengineering/server-core' const ctx = initStatisticsContext('analytics-collector', { @@ -46,35 +39,36 @@ configureAnalytics(config.SentryDSN, config) Analytics.setTag('application', 'analytics-collector-service') export const main = async (): Promise => { - setMetadata(serverToken.metadata.Secret, config.Secret) - setMetadata(serverClient.metadata.Endpoint, config.AccountsUrl) - setMetadata(serverClient.metadata.UserAgent, config.ServiceID) - - ctx.info('Analytics service started', { - accountsUrl: config.AccountsUrl, - supportWorkspace: config.SupportWorkspace - }) - - registerLoaders() - - const db = await getDB() - const collector = new Collector(ctx, db) - - const app = createServer(collector) - const server = listen(app, config.Port) - - const shutdown = (): void => { - void collector.close() - void closeDB() - server.close(() => process.exit()) - } - - process.on('SIGINT', shutdown) - process.on('SIGTERM', shutdown) - process.on('uncaughtException', (e) => { - console.error(e) - }) - process.on('unhandledRejection', (e) => { - console.error(e) - }) + ctx.info('Analytics collector service is not implemented yet') + process.exit() + // setMetadata(serverToken.metadata.Secret, config.Secret) + // setMetadata(serverClient.metadata.Endpoint, config.AccountsUrl) + // setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + // + // ctx.info('Analytics service started', { + // accountsUrl: config.AccountsUrl + // }) + // + // registerLoaders() + // + // const db = await getDB() + // const collector = new Collector(ctx, db) + // + // const app = createServer(collector) + // const server = listen(app, config.Port) + // + // const shutdown = (): void => { + // void collector.close() + // void closeDB() + // server.close(() => process.exit()) + // } + // + // process.on('SIGINT', shutdown) + // process.on('SIGTERM', shutdown) + // process.on('uncaughtException', (e) => { + // console.error(e) + // }) + // process.on('unhandledRejection', (e) => { + // console.error(e) + // }) } diff --git a/services/analytics-collector/pod-analytics-collector/src/server.ts b/services/analytics-collector/pod-analytics-collector/src/server.ts index 4ab69f30e76..0c87f6a6cff 100644 --- a/services/analytics-collector/pod-analytics-collector/src/server.ts +++ b/services/analytics-collector/pod-analytics-collector/src/server.ts @@ -22,7 +22,6 @@ import { extractToken } from '@hcengineering/server-client' import { ApiError } from './error' import { Collector } from './collector' -import { Action } from './types' type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise @@ -90,35 +89,6 @@ export function createServer (collector: Collector): Express { }) ) - app.post( - '/action', - wrapRequest(async (req, res, token) => { - if (req.body == null || Array.isArray(req.body)) { - throw new ApiError(400) - } - - const name = req.body.name - const messageId = req.body.messageId - const channelId = req.body.channelId - const _id = req.body._id - - if (name == null || messageId == null || channelId == null || _id == null) { - throw new ApiError(400) - } - - const action: Action = { - _id, - name, - messageId, - channelId - } - await collector.processAction(action, token) - - res.status(200) - res.json({}) - }) - ) - app.use((err: any, _req: any, res: any, _next: any) => { console.log(err) if (err instanceof ApiError) { diff --git a/services/analytics-collector/pod-analytics-collector/src/supportWsClient.ts b/services/analytics-collector/pod-analytics-collector/src/supportWsClient.ts index 834c4539fcb..6f046714504 100644 --- a/services/analytics-collector/pod-analytics-collector/src/supportWsClient.ts +++ b/services/analytics-collector/pod-analytics-collector/src/supportWsClient.ts @@ -15,7 +15,7 @@ import analyticsCollector, { AnalyticEvent, OnboardingChannel } from '@hcengineering/analytics-collector' import chunter, { Channel, ChatMessage } from '@hcengineering/chunter' -import { includesAny, getAllSocialStringsByPersonId, getPrimarySocialId, type Person } from '@hcengineering/contact' +import { getPrimarySocialId, type Person } from '@hcengineering/contact' import core, { Doc, generateId, @@ -39,7 +39,7 @@ import { generateToken } from '@hcengineering/server-token' import { getClient as getAccountClient } from '@hcengineering/account-client' import { Collection } from 'mongodb' import { eventToMarkup, getOnboardingMessage } from './format' -import { Action, MessageActions, OnboardingMessage } from './types' +import { OnboardingMessage } from './types' import config from './config' import { WorkspaceClient } from './workspaceClient' @@ -142,71 +142,6 @@ export class SupportWsClient extends WorkspaceClient { } } - async handleAcceptAction ( - action: Action, - personId: PersonId, - onboardingMessages: Collection - ): Promise { - if (action.channelId !== analyticsCollector.space.GeneralOnboardingChannel) { - return - } - - const client = await this.opClient - const personIds = await getAllSocialStringsByPersonId(client, personId) - - if (personIds.length === 0) { - return - } - - if (this.generalChannel === undefined) { - return - } - - if (!includesAny(this.generalChannel.members, personIds)) { - return - } - - const message = (await onboardingMessages.findOne({ messageId: action.messageId })) ?? undefined - - if (message === undefined) { - return - } - - await client.updateDoc(analyticsCollector.class.OnboardingChannel, core.space.Space, message.channelId, { - $push: { members: personId } - }) - - await onboardingMessages.deleteOne({ messageId: action.messageId }) - await client.removeCollection( - chunter.class.InlineButton, - analyticsCollector.space.GeneralOnboardingChannel, - action._id, - action.messageId, - chunter.class.ChatMessage, - 'inlineButtons' - ) - } - - async processAction ( - action: Action, - person: Person, - onboardingMessages: Collection - ): Promise { - switch (action.name) { - case MessageActions.Accept: { - const personId = await this.getPersonId(person._id) - - if (personId === undefined) { - return - } - - await this.handleAcceptAction(action, personId, onboardingMessages) - break - } - default: - } - } - async getPersonId (person: Ref): Promise { const cachedPersonId = this.personIdByPerson.get(person) const personId = cachedPersonId ?? (await getPrimarySocialId(await this.opClient, person)) @@ -260,19 +195,6 @@ export class SupportWsClient extends WorkspaceClient { messageId ) - await op.addCollection( - chunter.class.InlineButton, - analyticsCollector.space.GeneralOnboardingChannel, - messageId, - chunter.class.ChatMessage, - 'inlineButtons', - { - name: MessageActions.Accept, - title: 'Accept', - action: analyticsCollector.function.AnalyticsCollectorInlineAction - } - ) - await onboardingMessages.insertOne({ messageId, channelId }) } diff --git a/services/analytics-collector/pod-analytics-collector/src/types.ts b/services/analytics-collector/pod-analytics-collector/src/types.ts index 52290e05092..59241fff3b0 100644 --- a/services/analytics-collector/pod-analytics-collector/src/types.ts +++ b/services/analytics-collector/pod-analytics-collector/src/types.ts @@ -1,18 +1,7 @@ -import { ChatMessage, InlineButton, Channel } from '@hcengineering/chunter' +import { ChatMessage } from '@hcengineering/chunter' import { Ref } from '@hcengineering/core' import { OnboardingChannel } from '@hcengineering/analytics-collector' -export interface Action { - _id: Ref - name: string - messageId: Ref - channelId: Ref -} - -export enum MessageActions { - Accept = 'accept' -} - export interface OnboardingMessage { messageId: Ref channelId: Ref diff --git a/workers/transactor/src/transactor.ts b/workers/transactor/src/transactor.ts index c813387e11e..ff19d98586b 100644 --- a/workers/transactor/src/transactor.ts +++ b/workers/transactor/src/transactor.ts @@ -99,7 +99,6 @@ export class Transactor extends DurableObject { setMetadata(serverNotification.metadata.SesUrl, env.SES_URL ?? '') setMetadata(serverNotification.metadata.SesAuthToken, env.SES_AUTH_TOKEN) setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL) - setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE) setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL) registerTxAdapterFactory('postgresql', createPostgresTxAdapter, true) diff --git a/workers/transactor/worker-configuration.d.ts b/workers/transactor/worker-configuration.d.ts index 6adeb9a62ca..fd4cf0ba667 100644 --- a/workers/transactor/worker-configuration.d.ts +++ b/workers/transactor/worker-configuration.d.ts @@ -25,7 +25,6 @@ interface Env { SES_URL?: string SES_AUTH_TOKEN?: string - SUPPORT_WORKSPACE?: string TELEGRAM_BOT_URL: string AI_BOT_URL?: string LAST_NAME_FIRST?: string diff --git a/workers/transactor/wrangler.toml b/workers/transactor/wrangler.toml index f440cecfa96..89a1995d17f 100644 --- a/workers/transactor/wrangler.toml +++ b/workers/transactor/wrangler.toml @@ -33,7 +33,6 @@ ENABLE_COMPRESSION=true # PUSH_PUBLIC_KEY # PUSH_PRIVATE_KEY # SENTRY_DSN -# SUPPORT_WORKSPACE # TELEGRAM_BOT_URL # AI_BOT_URL # LAST_NAME_FIRST