mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 20:27:43 +02:00
Merge branch 'develop' into staging-new
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Vendored
-2
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "desktop",
|
||||
"version": "0.6.271",
|
||||
"version": "0.6.435",
|
||||
"main": "dist/main/electron.js",
|
||||
"author": "Hardcore Engineering <hey@huly.io>",
|
||||
"template": "@hcengineering/default-package",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+16
-18
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -201,7 +201,7 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
|
||||
const socialIdByAccount = await getSocialIdByOldAccount(client)
|
||||
|
||||
ctx.info('processing activity reactions ', {})
|
||||
const iterator = await client.traverse(DOMAIN_ACTIVITY, { _class: activity.class.Reaction })
|
||||
const iterator = await client.traverse(DOMAIN_REACTION, { _class: activity.class.Reaction })
|
||||
|
||||
try {
|
||||
let processed = 0
|
||||
@@ -228,7 +228,7 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
|
||||
}
|
||||
|
||||
if (operations.length > 0) {
|
||||
await client.bulk(DOMAIN_ACTIVITY, operations)
|
||||
await client.bulk(DOMAIN_REACTION, operations)
|
||||
}
|
||||
|
||||
processed += docs.length
|
||||
@@ -249,9 +249,9 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<vo
|
||||
au: DocAttributeUpdates,
|
||||
update: MigrateUpdate<DocUpdateMessage>['attributeUpdates'],
|
||||
field: P
|
||||
): boolean {
|
||||
): void {
|
||||
const oldValue = au?.[field]
|
||||
if (oldValue == null) return false
|
||||
if (oldValue == null) return
|
||||
|
||||
let changed = false
|
||||
let newValue: any
|
||||
@@ -275,8 +275,6 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<vo
|
||||
|
||||
update[field] = newValue
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
const iterator = await client.traverse(DOMAIN_ACTIVITY, {
|
||||
@@ -301,15 +299,14 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<vo
|
||||
for (const doc of docs) {
|
||||
const dum = doc as DocUpdateMessage
|
||||
if (dum.attributeUpdates == null) continue
|
||||
let changed = false
|
||||
const update: any = { attributeUpdates: { ...dum.attributeUpdates } }
|
||||
|
||||
changed = migrateField(dum.attributeUpdates, update.attributeUpdates, 'added') || changed
|
||||
changed = migrateField(dum.attributeUpdates, update.attributeUpdates, 'prevValue') || changed
|
||||
changed = migrateField(dum.attributeUpdates, update.attributeUpdates, 'removed') || changed
|
||||
changed = migrateField(dum.attributeUpdates, update.attributeUpdates, 'set') || changed
|
||||
migrateField(dum.attributeUpdates, update.attributeUpdates, 'added')
|
||||
migrateField(dum.attributeUpdates, update.attributeUpdates, 'prevValue')
|
||||
migrateField(dum.attributeUpdates, update.attributeUpdates, 'removed')
|
||||
migrateField(dum.attributeUpdates, update.attributeUpdates, 'set')
|
||||
|
||||
if (!changed) continue
|
||||
update.attributeUpdates.attrClass = core.class.TypePersonId
|
||||
|
||||
operations.push({
|
||||
filter: { _id: dum._id },
|
||||
@@ -375,11 +372,11 @@ export const activityOperation: MigrateOperation = {
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'accounts-to-social-ids',
|
||||
state: 'accounts-to-social-ids-v2',
|
||||
func: migrateAccountsToSocialIds
|
||||
},
|
||||
{
|
||||
state: 'accounts-in-doc-updates',
|
||||
state: 'accounts-in-doc-updates-v2',
|
||||
func: migrateAccountsInDocUpdates
|
||||
}
|
||||
])
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SocialChannelProvider>
|
||||
|
||||
@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<DocNotifyContext>[]
|
||||
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<InlineButtonAction>
|
||||
}
|
||||
|
||||
@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TTypingInfo extends TDoc implements TypingInfo {
|
||||
objectId!: Ref<Doc>
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
person!: Ref<Person>
|
||||
lastTyping!: Timestamp
|
||||
}
|
||||
|
||||
@Model(chunter.class.ChunterExtension, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TChunterExtension extends TDoc implements ChunterExtension {
|
||||
ofClass!: Ref<Class<Doc>>
|
||||
point!: ChunterExtensionPoint
|
||||
component!: AnyComponent
|
||||
}
|
||||
|
||||
@@ -13,50 +13,15 @@
|
||||
// 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<ChatMessage>
|
||||
parentMessageId?: Ref<ChatMessage>
|
||||
}
|
||||
|
||||
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,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: notification.class.MentionInboxNotification
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverAiBot.trigger.OnMessageNotified,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: notification.class.ActivityInboxNotification
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverAiBot.trigger.OnUserStatus,
|
||||
txMatch: {
|
||||
@@ -64,4 +29,13 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverAiBot.trigger.OnMessageSend,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: chunter.class.ChatMessage
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ModernButton } from '@hcengineering/ui'
|
||||
import aiBot, { OnboardingEvent, type OnboardingEventRequest, OpenChatInSidebarData } from '@hcengineering/ai-bot'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
|
||||
export let object: OnboardingChannel
|
||||
|
||||
async function handleClick (): Promise<void> {
|
||||
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
|
||||
if (url === '' || token === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const req: OnboardingEventRequest<OpenChatInSidebarData> = {
|
||||
event: OnboardingEvent.OpenChatInSidebar,
|
||||
data: {
|
||||
personId: object.socialString,
|
||||
workspace: object.workspaceId
|
||||
}
|
||||
}
|
||||
await fetch(concatLink(url, '/onboarding'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(req)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<ModernButton label={chunter.string.OpenChatInSidebar} size="small" on:click={handleClick} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
}
|
||||
</style>
|
||||
@@ -14,12 +14,7 @@
|
||||
//
|
||||
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import OnboardingChannelPanelExtension from './components/OnboardingChannelAsideExtension.svelte'
|
||||
|
||||
export * from './requests'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
OnboardingChannelPanelExtension
|
||||
}
|
||||
})
|
||||
export default async (): Promise<Resources> => ({})
|
||||
|
||||
@@ -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<ChatMessage>
|
||||
parentMessageId?: Ref<ChatMessage>
|
||||
}
|
||||
|
||||
const aiBot = plugin(aiBotId, {
|
||||
metadata: {
|
||||
EndpointURL: '' as Metadata<string>
|
||||
},
|
||||
mixin: {
|
||||
TransferredMessage: '' as Ref<Mixin<TransferredMessage>>
|
||||
},
|
||||
account: {
|
||||
AIBot: '' as PersonId
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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<Class<ChatMessage>>
|
||||
messageId: Ref<ChatMessage>
|
||||
message: string
|
||||
createdOn: Timestamp
|
||||
}
|
||||
|
||||
export interface AIMessageEventRequest extends AIEventRequest {
|
||||
objectId: Ref<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
objectId: Ref<Doc>
|
||||
objectSpace: Ref<Space>
|
||||
user: PersonId
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface AITransferEventRequest extends AIEventRequest {
|
||||
toPersonId: PersonId
|
||||
toWorkspace: WorkspaceUuid
|
||||
fromWorkspace: WorkspaceUuid
|
||||
fromWorkspaceName: string
|
||||
fromWorkspaceUrl: string
|
||||
parentMessageId?: Ref<ChatMessage>
|
||||
collection: string
|
||||
createdOn: Timestamp
|
||||
}
|
||||
|
||||
export interface TranslateRequest {
|
||||
|
||||
@@ -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<T = Record<string, any>> {
|
||||
event: OnboardingEvent
|
||||
data: T
|
||||
}
|
||||
@@ -14,10 +14,5 @@
|
||||
//
|
||||
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import { AnalyticsCollectorInlineAction } from './utils'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
function: {
|
||||
AnalyticsCollectorInlineAction
|
||||
}
|
||||
})
|
||||
export default async (): Promise<Resources> => ({})
|
||||
|
||||
@@ -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<void> => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Channel>
|
||||
},
|
||||
function: {
|
||||
AnalyticsCollectorInlineAction: '' as Resource<InlineButtonAction>
|
||||
},
|
||||
string: {
|
||||
OnboardingChannelDescription: '' as IntlString,
|
||||
Error: '' as IntlString,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ChunterExtension, ChunterExtensionPoint } from '@hcengineering/chunter'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let object: Doc
|
||||
export let point: ChunterExtensionPoint
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let extensions: ChunterExtension[] = []
|
||||
|
||||
$: extensions = client.getModel().findAllSync(chunter.class.ChunterExtension, { ofClass: object._class, point })
|
||||
</script>
|
||||
|
||||
{#each extensions as extension}
|
||||
<Component is={extension.component} props={{ object }} />
|
||||
{/each}
|
||||
@@ -1,49 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ModernButton } from '@hcengineering/ui'
|
||||
import chunter, { ChatMessage, InlineButton } from '@hcengineering/chunter'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
export let value: ChatMessage
|
||||
export let inlineButtons: InlineButton[] = []
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
$: if ((value.inlineButtons ?? 0) > 0 && inlineButtons.length === 0) {
|
||||
query.query(chunter.class.InlineButton, { attachedTo: value._id, space: value.space }, (res) => {
|
||||
inlineButtons = res
|
||||
})
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
}
|
||||
|
||||
async function handleInlineButtonClick (button: InlineButton): Promise<void> {
|
||||
const resource = await getResource(button.action)
|
||||
await resource(button, value._id, value.attachedTo)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each inlineButtons as button}
|
||||
<ModernButton
|
||||
title={button.title}
|
||||
label={button.titleIntl}
|
||||
size="small"
|
||||
on:click={() => {
|
||||
void handleInlineButtonClick(button)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
@@ -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<ChatMessage> | 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 @@
|
||||
<ShowMore limit={compact ? 80 : undefined}>
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={displayText} />
|
||||
{#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0}
|
||||
{#if (value.attachments ?? 0) > 0}
|
||||
<div class="mt-2" />
|
||||
{/if}
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} {videoPreload} {isOwn} />
|
||||
<InlineButtons {value} {inlineButtons} />
|
||||
</div>
|
||||
</ShowMore>
|
||||
{:else}
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={displayText} />
|
||||
{#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0}
|
||||
{#if (value.attachments ?? 0) > 0}
|
||||
<div class="mt-2" />
|
||||
{/if}
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} {videoPreload} {isOwn} />
|
||||
<InlineButtons {value} {inlineButtons} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if object}
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
<ChunterExtensionComponent {object} point="aside" />
|
||||
<slot />
|
||||
</Scroller>
|
||||
|
||||
@@ -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<SocialChannelProvider>
|
||||
inlineButtons?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,22 +83,6 @@ export interface ChatSyncInfo extends Doc {
|
||||
timestamp: Timestamp
|
||||
}
|
||||
|
||||
export interface TypingInfo extends Doc {
|
||||
objectId: Ref<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
person: Ref<Person>
|
||||
lastTyping: Timestamp
|
||||
}
|
||||
|
||||
export type InlineButtonAction = (button: InlineButton, message: Ref<ChatMessage>, channel: Ref<Doc>) => Promise<void>
|
||||
|
||||
export interface InlineButton extends AttachedDoc {
|
||||
name: string
|
||||
titleIntl?: IntlString
|
||||
title?: string
|
||||
action: Resource<InlineButtonAction>
|
||||
}
|
||||
|
||||
export interface ChatWidgetTab extends WidgetTab {
|
||||
data: {
|
||||
_id?: Ref<Doc>
|
||||
@@ -111,13 +94,6 @@ export interface ChatWidgetTab extends WidgetTab {
|
||||
}
|
||||
}
|
||||
|
||||
export type ChunterExtensionPoint = 'aside'
|
||||
export interface ChunterExtension extends Doc {
|
||||
ofClass: Ref<Class<Doc>>
|
||||
point: ChunterExtensionPoint
|
||||
component: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -162,10 +138,7 @@ export default plugin(chunterId, {
|
||||
DirectMessage: '' as Ref<Class<DirectMessage>>,
|
||||
ChatMessage: '' as Ref<Class<ChatMessage>>,
|
||||
ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>>,
|
||||
ChatSyncInfo: '' as Ref<Class<ChatSyncInfo>>,
|
||||
InlineButton: '' as Ref<Class<InlineButton>>,
|
||||
TypingInfo: '' as Ref<Class<TypingInfo>>,
|
||||
ChunterExtension: '' as Ref<Class<ChunterExtension>>
|
||||
ChatSyncInfo: '' as Ref<Class<ChatSyncInfo>>
|
||||
},
|
||||
mixin: {
|
||||
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>
|
||||
|
||||
@@ -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<GlobalPerson | undefined>
|
||||
): Promise<Ref<Employee> | null> {
|
||||
const txFactory = new TxFactory(me.primarySocialId)
|
||||
const personByUuid = await client.findOne(contact.class.Person, { personUuid: me.uuid })
|
||||
let personRef: Ref<Person> | 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<Employee> })
|
||||
|
||||
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<Employee>
|
||||
}
|
||||
|
||||
@@ -113,10 +113,13 @@ export async function getToken (
|
||||
}
|
||||
|
||||
function getTokenRoomName (roomName: string, roomId: Ref<Room>): 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({
|
||||
|
||||
@@ -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<Client | undefined> {
|
||||
}
|
||||
|
||||
// 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<Client | undefined> {
|
||||
return newClient
|
||||
}
|
||||
|
||||
async function ensureEmployee (
|
||||
ctx: MeasureContext,
|
||||
me: Account,
|
||||
client: Client,
|
||||
socialIds: SocialId[]
|
||||
): Promise<Ref<Employee> | null> {
|
||||
const txFactory = new TxFactory(me.primarySocialId)
|
||||
const personByUuid = await client.findOne(contact.class.Person, { personUuid: me.uuid })
|
||||
let personRef: Ref<Person> | undefined = personByUuid?._id
|
||||
if (personRef === undefined) {
|
||||
const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: { $in: me.socialIds } })
|
||||
async function getGlobalPerson (): Promise<GlobalPerson | undefined> {
|
||||
const getPerson = await getResource(login.function.GetPerson)
|
||||
const [status, globalPerson] = await getPerson()
|
||||
|
||||
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 (status !== OK) {
|
||||
console.error('Error getting global person')
|
||||
return undefined
|
||||
}
|
||||
|
||||
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<Employee> })
|
||||
|
||||
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<Employee>
|
||||
return globalPerson
|
||||
}
|
||||
|
||||
export function clearMetadata (ws: string): void {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxUpdateDoc,
|
||||
combineAttributes
|
||||
combineAttributes,
|
||||
ArrOf
|
||||
} from '@hcengineering/core'
|
||||
import core from '@hcengineering/core/src/component'
|
||||
import notification from '@hcengineering/notification'
|
||||
@@ -267,6 +268,8 @@ export async function getTxAttributesUpdates (
|
||||
|
||||
if (clazz !== undefined && 'to' in clazz.type) {
|
||||
attrClass = clazz.type.to as Ref<Class<Doc>>
|
||||
} else if (clazz !== undefined && hierarchy.isDerived(clazz.type._class, core.class.ArrOf)) {
|
||||
attrClass = (clazz.type as ArrOf<Doc>).of._class
|
||||
} else if (clazz !== undefined && 'of' in clazz?.type) {
|
||||
attrClass = (clazz.type.of as RefTo<Doc>).to
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
// 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
|
||||
// }
|
||||
async function OnUserStatus (txes: TxCUD<UserStatus>[], control: TriggerControl): Promise<Tx[]> {
|
||||
if (!hasAiEndpoint()) {
|
||||
return []
|
||||
}
|
||||
|
||||
// const personAccounts = await control.modelDb.findAll(contact.class.PersonAccount, {
|
||||
// _id: { $in: members as PersonId[] }
|
||||
// })
|
||||
// const persons = new Set(personAccounts.map((account) => account.person))
|
||||
if (control.txFactory.account === aiBotEmailSocialId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// return persons.size === 2
|
||||
// }
|
||||
for (const tx of txes) {
|
||||
if (![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise<Doc | undefined> {
|
||||
// if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
// const thread = message as ThreadMessage
|
||||
// const _id = thread.objectId
|
||||
// const _class = thread.objectClass
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const createTx = tx as TxCreateDoc<UserStatus>
|
||||
const status = TxProcessor.createDoc2Doc(createTx)
|
||||
if (status.user === systemAccountUuid) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// return (await control.findAll(control.ctx, _class, { _id }))[0]
|
||||
// } else {
|
||||
// const _id = message.attachedTo
|
||||
// const _class = message.attachedToClass
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = tx as TxUpdateDoc<UserStatus>
|
||||
const val = updateTx.operations.online
|
||||
if (val !== true) {
|
||||
continue
|
||||
}
|
||||
|
||||
// return (await control.findAll(control.ctx, _class, { _id }))[0]
|
||||
// }
|
||||
// }
|
||||
const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0]
|
||||
if (status === undefined || status.user === systemAccountUuid) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
const aiBotPerson = await getPerson(control, aiBotEmailSocialId)
|
||||
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
if (aiBotPerson === undefined) {
|
||||
await createAccountRequest(control.workspace.uuid, control.ctx)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise<Ref<ChatMessage> | undefined> {
|
||||
// if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) {
|
||||
// return undefined
|
||||
// }
|
||||
return []
|
||||
}
|
||||
|
||||
// const parentInfo = (
|
||||
// await control.findAll(control.ctx, message.attachedToClass, {
|
||||
// _id: message.attachedTo as Ref<ChatMessage>,
|
||||
// [aiBot.mixin.TransferredMessage]: { $exists: true }
|
||||
// })
|
||||
// )[0]
|
||||
async function OnMessageSend (originTxs: TxCreateDoc<ChatMessage>[], control: TriggerControl): Promise<Tx[]> {
|
||||
if (!hasAiEndpoint()) {
|
||||
return []
|
||||
}
|
||||
|
||||
// if (parentInfo !== undefined) {
|
||||
// return control.hierarchy.as(parentInfo, aiBot.mixin.TransferredMessage).messageId
|
||||
// }
|
||||
const { hierarchy } = control
|
||||
const txes = originTxs.filter((it) => it.modifiedBy !== aiBotEmailSocialId)
|
||||
|
||||
// return message.attachedTo as Ref<ChatMessage>
|
||||
// }
|
||||
if (txes.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// async function createTransferEvent (
|
||||
// control: TriggerControl,
|
||||
// message: ChatMessage,
|
||||
// account: any,
|
||||
// data: AIMessageEventRequest
|
||||
// ): Promise<AITransferEventRequest | undefined> {
|
||||
// if (account.role !== AccountRole.Owner) {
|
||||
// return
|
||||
// }
|
||||
for (const tx of txes) {
|
||||
const message = TxProcessor.createDoc2Doc(tx)
|
||||
|
||||
// const supportWorkspaceId = getSupportWorkspaceId()
|
||||
const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)
|
||||
const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass
|
||||
|
||||
// if (supportWorkspaceId === undefined) {
|
||||
// return
|
||||
// }
|
||||
if (!hierarchy.isDerived(docClass, chunter.class.DirectMessage)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
if (docClass === chunter.class.DirectMessage) {
|
||||
await onBotDirectMessageSend(control, message)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise<Doc | undefined> {
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise<boolean> {
|
||||
const { members } = direct
|
||||
|
||||
if (!members.includes(aiBotEmailSocialId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const persons = await getPersons(control, members)
|
||||
|
||||
const uuids = new Set(
|
||||
persons.map((account) => account.personUuid).filter((uuid): uuid is PersonUuid => uuid !== undefined)
|
||||
)
|
||||
|
||||
return uuids.size === 2
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise<void> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function onSupportWorkspaceMessage (control: TriggerControl, message: ChatMessage): Promise<void> {
|
||||
// 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<AttachedDoc>[], control: TriggerControl): Promise<Tx[]> {
|
||||
// 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<ChatMessage>)
|
||||
//
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnMention (tx: TxCreateDoc<MentionInboxNotification>[], control: TriggerControl): Promise<Tx[]> {
|
||||
// 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<ChatMessage>(mention.mentionedInClass, { _id: mention.mentionedIn as Ref<ChatMessage> })
|
||||
// )[0]
|
||||
//
|
||||
// if (message === undefined) {
|
||||
// return []
|
||||
// }
|
||||
//
|
||||
// await createResponseEvent(message, control)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnMessageNotified (
|
||||
tx: TxCreateDoc<ActivityInboxNotification>[],
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
// 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<ChatMessage>(doc.attachedToClass, { _id: doc.attachedTo as Ref<ChatMessage> })
|
||||
// )[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 []
|
||||
}
|
||||
|
||||
export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
// TODO: FIXME
|
||||
return [] // Not implemented
|
||||
// for (const originTx of txes) {
|
||||
// const tx = originTx as TxCUD<UserStatus>
|
||||
|
||||
// if (
|
||||
// tx.objectClass !== core.class.UserStatus ||
|
||||
// ![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class)
|
||||
// ) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if (tx._class === core.class.TxCreateDoc) {
|
||||
// const createTx = tx as TxCreateDoc<UserStatus>
|
||||
// const status = TxProcessor.createDoc2Doc(createTx)
|
||||
// if (status.user === aiBot.account.AIBot || status.user === core.account.System || !status.online) {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (tx._class === core.class.TxUpdateDoc) {
|
||||
// const updateTx = tx as TxUpdateDoc<UserStatus>
|
||||
// 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 === aiBot.account.AIBot || status.user === core.account.System) {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// const account = control.modelDb.findAllSync(contact.class.PersonAccount, { email: aiBotAccountEmail })[0]
|
||||
|
||||
// if (account !== undefined) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// await createAccountRequest(control.workspace, control.ctx)
|
||||
// }
|
||||
|
||||
// return []
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,18 +15,31 @@
|
||||
|
||||
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)
|
||||
export async function createAccountRequest (workspace: WorkspaceUuid, ctx: MeasureContext): Promise<void> {
|
||||
const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? ''
|
||||
|
||||
if (supportWorkspaceId === '') {
|
||||
return undefined
|
||||
if (url === '') {
|
||||
return
|
||||
}
|
||||
|
||||
return supportWorkspaceId
|
||||
try {
|
||||
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({})
|
||||
})
|
||||
} catch (err) {
|
||||
ctx.error('Could not send create ai account request', { err })
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendAIEvents (
|
||||
@@ -54,25 +67,7 @@ export async function sendAIEvents (
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAccountRequest (workspace: WorkspaceUuid, ctx: MeasureContext): Promise<void> {
|
||||
export function hasAiEndpoint (): boolean {
|
||||
const url = getMetadata(serverAIBot.metadata.EndpointURL) ?? ''
|
||||
|
||||
if (url === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
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({})
|
||||
})
|
||||
} catch (err) {
|
||||
ctx.error('Could not send create ai account request', { err })
|
||||
}
|
||||
return url !== ''
|
||||
}
|
||||
|
||||
@@ -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<string>,
|
||||
EndpointURL: '' as Metadata<string>
|
||||
},
|
||||
trigger: {
|
||||
OnMessageSend: '' as Resource<TriggerFunc>,
|
||||
OnMention: '' as Resource<TriggerFunc>,
|
||||
OnUserStatus: '' as Resource<TriggerFunc>,
|
||||
OnMessageNotified: '' as Resource<TriggerFunc>
|
||||
OnMessageSend: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<Config> = {
|
||||
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 ?? '',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<WorkspaceUuid, NodeJS.Timeout> = new Map<WorkspaceUuid, NodeJS.Timeout>()
|
||||
private readonly connectingWorkspaces = new Map<WorkspaceUuid, Promise<void>>()
|
||||
|
||||
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<WorkspaceInfoRecord> {
|
||||
return (await this.storage.getWorkspace(workspace)) ?? { workspace: config.SupportWorkspace }
|
||||
}
|
||||
|
||||
async connectSupportWorkspace (): Promise<void> {
|
||||
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<WorkspaceInfoRecord | undefined> {
|
||||
return await this.storage.getWorkspace(workspace)
|
||||
}
|
||||
|
||||
async closeWorkspaceClient (workspace: WorkspaceUuid): Promise<void> {
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
if (this.supportClient === undefined) return
|
||||
|
||||
await this.supportClient.transferAIReply(response, data)
|
||||
}
|
||||
|
||||
async transfer (event: AITransferEventRequest): Promise<void> {
|
||||
const workspace = event.toWorkspace
|
||||
|
||||
if (workspace === config.SupportWorkspace) {
|
||||
if (this.supportClient === undefined) return
|
||||
|
||||
await this.supportClient.transfer(event)
|
||||
return
|
||||
}
|
||||
|
||||
const wsClient = await this.getWorkspaceClient(workspace)
|
||||
if (wsClient === undefined) return
|
||||
|
||||
await wsClient.transfer(event)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
for (const workspace of this.workspaces.values()) {
|
||||
await workspace.close()
|
||||
@@ -230,42 +173,18 @@ export class AIControl {
|
||||
this.workspaces.clear()
|
||||
}
|
||||
|
||||
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
|
||||
const record = await this.storage.getWorkspace(workspace)
|
||||
|
||||
if (record === undefined) {
|
||||
await this.storage.addWorkspace({ workspace, avatarPath: path, avatarLastModified: lastModified })
|
||||
} else {
|
||||
await this.storage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } })
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkspaceClient (workspace: WorkspaceUuid): Promise<WorkspaceClient | undefined> {
|
||||
await this.initWorkspaceClient(workspace)
|
||||
|
||||
return this.workspaces.get(workspace)
|
||||
}
|
||||
|
||||
async openChatInSidebar (data: OpenChatInSidebarData): Promise<void> {
|
||||
const wsClient = await this.getWorkspaceClient(data.workspace)
|
||||
if (wsClient === undefined) return
|
||||
await wsClient.openAIChatInSidebar(data.personId)
|
||||
}
|
||||
|
||||
async processOnboardingEvent (event: OnboardingEventRequest): Promise<void> {
|
||||
switch (event.event) {
|
||||
case OnboardingEvent.OpenChatInSidebar:
|
||||
await this.openChatInSidebar(event.data as OpenChatInSidebarData)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async translate (req: TranslateRequest): Promise<TranslateResponse | undefined> {
|
||||
if (this.aiClient === undefined) {
|
||||
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<void> {
|
||||
const wsClient = await this.getWorkspaceClient(workspace)
|
||||
if (wsClient === undefined) return
|
||||
|
||||
await wsClient.processMessageEvent(event)
|
||||
}
|
||||
|
||||
async processEvent (workspace: WorkspaceUuid, events: AIEventRequest[]): Promise<void> {
|
||||
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<void> {
|
||||
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<Room> | 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<Room>)
|
||||
await wsClient.processLoveTranscript(request.transcript, request.participant, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
|
||||
// res.status(200)
|
||||
// res.json(resp)
|
||||
})
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/onboarding',
|
||||
wrapRequest(async (req, res) => {
|
||||
if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') {
|
||||
throw new ApiError(400)
|
||||
if (token.account !== controller.personUuid) {
|
||||
throw new ApiError(401)
|
||||
}
|
||||
|
||||
await controller.processOnboardingEvent(req.body as OnboardingEventRequest)
|
||||
const roomName = req.params.roomName
|
||||
const resp = await controller.getLoveIdentity(roomName)
|
||||
|
||||
if (resp === undefined) {
|
||||
throw new ApiError(404)
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
res.json({})
|
||||
res.json(resp)
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> = async () => {
|
||||
clientRef.close()
|
||||
export async function getDbStorage (): Promise<DbStorage> {
|
||||
const db = await connectDB()
|
||||
return new DbStorage(db)
|
||||
}
|
||||
|
||||
export class DbStorage {
|
||||
@@ -72,4 +73,8 @@ export class DbStorage {
|
||||
async updateWorkspace (workspace: string, update: UpdateFilter<WorkspaceInfoRecord>): Promise<void> {
|
||||
await this.workspacesInfoCollection.updateOne({ workspace }, update)
|
||||
}
|
||||
|
||||
close (): void {
|
||||
clientRef.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
role: string
|
||||
user: PersonId
|
||||
user: PersonUuid
|
||||
tokens: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface AIReplyTransferData {
|
||||
messageClass: Ref<Class<ChatMessage>>
|
||||
email: string
|
||||
fromWorkspace: string
|
||||
originalMessageId: Ref<ChatMessage>
|
||||
originalParent?: Ref<ChatMessage>
|
||||
}
|
||||
|
||||
@@ -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<WorkspaceInfoWithStatus | undefined> {
|
||||
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<WorkspaceInfoWithStatus | undefined> {
|
||||
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<WorkspaceInfoWithStatus | undefined> {
|
||||
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<string, NodeJS.Timeout>()
|
||||
const attemptsByWorkspace = new Map<string, number>()
|
||||
|
||||
export async function tryAssignToWorkspace (
|
||||
workspace: WorkspaceUuid,
|
||||
ctx: MeasureContext,
|
||||
clearAttempts = true
|
||||
): Promise<boolean> {
|
||||
if (clearAttempts) {
|
||||
attemptsByWorkspace.delete(workspace)
|
||||
export async function getGlobalPerson (token: string): Promise<GlobalPerson | undefined> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<PersonUuid | undefined> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
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 {
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
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<Document> })
|
||||
|
||||
@@ -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,
|
||||
TxCreateDoc,
|
||||
TxUpdateDoc,
|
||||
MeasureContext,
|
||||
Markup,
|
||||
MeasureContext,
|
||||
PersonId,
|
||||
Ref,
|
||||
SocialIdType,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxOperations,
|
||||
TxProcessor,
|
||||
TxUpdateDoc,
|
||||
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<Ref<Person>, PersonId>()
|
||||
private readonly meetingMinutes: MeetingMinutes[] = []
|
||||
|
||||
constructor (
|
||||
@@ -171,36 +173,56 @@ export class LoveController {
|
||||
this.connectedRooms.delete(roomId)
|
||||
}
|
||||
|
||||
async getSocialId (person: Ref<Person>): Promise<PersonId | undefined> {
|
||||
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<Person>, roomId: Ref<Room>): Promise<void> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const room = await this.getRoom(roomId)
|
||||
// const participant = await this.getRoomParticipant(roomId, person)
|
||||
const room = await this.getRoom(roomId)
|
||||
const participant = await this.getRoomParticipant(roomId, person)
|
||||
|
||||
// if (room === undefined || participant === undefined) {
|
||||
// return
|
||||
// }
|
||||
if (room === undefined || participant === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// const personAccount = this.client.getModel().getAccountByPersonId(participant.person)[0]
|
||||
// const doc = await this.getMeetingMinutes(room)
|
||||
const socialId = await this.getSocialId(person)
|
||||
if (socialId === undefined) return
|
||||
|
||||
// if (doc === undefined) return
|
||||
// const op = this.client.apply(undefined, undefined, true)
|
||||
const doc = await this.getMeetingMinutes(room)
|
||||
if (doc === undefined) return
|
||||
|
||||
// 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 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<Room>): string {
|
||||
function getTokenRoomName (workspace: WorkspaceUuid, roomName: string, roomId: Ref<Room>): string {
|
||||
return `${workspace}_${roomName}_${roomId}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Ref<OnboardingChannel>, OnboardingChannelMetadata>()
|
||||
readonly disableShowAiRepliesChannels = new Map<Ref<OnboardingChannel>, OnboardingChannelMetadata>()
|
||||
readonly assignedChannels = new Set<Ref<OnboardingChannel>>()
|
||||
|
||||
async initClient (): Promise<TxOperations> {
|
||||
// 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<Doc>[])
|
||||
for (const tx of txes) {
|
||||
switch (tx._class) {
|
||||
case core.class.TxUpdateDoc: {
|
||||
this.txUpdateDoc(client, tx as TxUpdateDoc<Doc>)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateChannels (tx: TxUpdateDoc<OnboardingChannel>): Promise<void> {
|
||||
// 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<Doc>): void {
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
if (hierarchy.isDerived(tx.objectClass, analyticsCollector.class.OnboardingChannel)) {
|
||||
void this.updateChannels(tx as TxUpdateDoc<OnboardingChannel>)
|
||||
}
|
||||
}
|
||||
|
||||
async transferAIReply (response: Markup, data: AIReplyTransferData): Promise<void> {
|
||||
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<Doc, ThreadMessage>(
|
||||
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<Class<ChatMessage>>,
|
||||
channel,
|
||||
aiBot.mixin.TransferredMessage,
|
||||
{
|
||||
messageId: data.originalMessageId,
|
||||
parentMessageId: data.originalParent
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const ref = await op.addCollection<Doc, ChatMessage>(
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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> | TxOperations
|
||||
|
||||
loginTimeout: NodeJS.Timeout | undefined
|
||||
loginDelayMs = 2 * 1000
|
||||
|
||||
channelByKey = new Map<string, Ref<OnboardingChannel>>()
|
||||
rate = new RateLimiter(1)
|
||||
|
||||
aiPerson: Person | undefined
|
||||
|
||||
typingMap: Map<Ref<Doc>, TypingInfo> = new Map<Ref<Doc>, TypingInfo>()
|
||||
typingTimeoutsMap: Map<Ref<Doc>, NodeJS.Timeout> = new Map<Ref<Doc>, NodeJS.Timeout>()
|
||||
directByPersonId = new Map<PersonId, Ref<DirectMessage>>()
|
||||
personUuidBySocialId = new Map<PersonId, PersonUuid>()
|
||||
|
||||
historyMap = new Map<Ref<Doc>, WithId<HistoryRecord>[]>()
|
||||
|
||||
@@ -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<void> {
|
||||
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<TxOperations> {
|
||||
await this.tryLogin()
|
||||
|
||||
private async initClient (): Promise<TxOperations> {
|
||||
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<Doc>[])
|
||||
}
|
||||
@@ -138,7 +131,7 @@ export class WorkspaceClient {
|
||||
return opClient
|
||||
}
|
||||
|
||||
private async uploadAvatarFile (client: TxOperations): Promise<void> {
|
||||
private async checkEmployeeInfo (client: TxOperations): Promise<void> {
|
||||
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<void> {
|
||||
this.ctx.info('Logging in: ', { workspace: this.workspace })
|
||||
const accountClient = getAccountClient()
|
||||
const token = await accountClient.login(aiBotAccount, config.Password)
|
||||
private async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
|
||||
const record = await this.dbStorage.getWorkspace(workspace)
|
||||
|
||||
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)
|
||||
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<void> {
|
||||
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<Blob>, avatarType: AvatarType.IMAGE })
|
||||
}
|
||||
|
||||
async getThreadParent (
|
||||
client: TxOperations,
|
||||
parentMessageId: Ref<ChatMessage>,
|
||||
_id: Ref<Doc>,
|
||||
_class: Ref<Class<Doc>>
|
||||
): Promise<ChatMessage | undefined> {
|
||||
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<Doc>,
|
||||
_class: Ref<Class<Doc>>,
|
||||
space: Ref<Space>,
|
||||
message: string
|
||||
): Promise<void> {
|
||||
const op = client.apply(undefined, 'AITransferEventRequest')
|
||||
if (event.messageClass === chunter.class.ChatMessage) {
|
||||
await this.startTyping(client, space, _id, _class)
|
||||
const ref = await op.addCollection<Doc, ChatMessage>(
|
||||
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<Doc, ThreadMessage>(
|
||||
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<Class<ChatMessage>>,
|
||||
space,
|
||||
aiBot.mixin.TransferredMessage,
|
||||
{
|
||||
messageId: event.messageId,
|
||||
parentMessageId: event.parentMessageId
|
||||
}
|
||||
)
|
||||
await this.finishTyping(client, parent._id)
|
||||
}
|
||||
}
|
||||
|
||||
await op.commit()
|
||||
}
|
||||
|
||||
clearTypingTimeout (objectId: Ref<Doc>): void {
|
||||
const currentTimeout = this.typingTimeoutsMap.get(objectId)
|
||||
|
||||
if (currentTimeout !== undefined) {
|
||||
clearTimeout(currentTimeout)
|
||||
this.typingTimeoutsMap.delete(objectId)
|
||||
}
|
||||
}
|
||||
|
||||
async startTyping (
|
||||
client: TxOperations,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<Doc>,
|
||||
objectClass: Ref<Class<Doc>>
|
||||
): Promise<void> {
|
||||
if (this.aiPerson === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clearTypingTimeout(objectId)
|
||||
const typingInfo = this.typingMap.get(objectId)
|
||||
|
||||
if (typingInfo === undefined) {
|
||||
const data: Data<TypingInfo> = {
|
||||
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<Doc>): Promise<void> {
|
||||
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<Doc>): Promise<WithId<HistoryRecord>[]> {
|
||||
private async getHistory (objectId: Ref<Doc>): Promise<WithId<HistoryRecord>[]> {
|
||||
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<HistoryRecord>[],
|
||||
user: PersonId,
|
||||
user: PersonUuid,
|
||||
objectId: Ref<Doc>,
|
||||
objectClass: Ref<Class<Doc>>
|
||||
): Promise<void> {
|
||||
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<Doc>,
|
||||
objectClass: Ref<Class<Doc>>
|
||||
): Promise<void> {
|
||||
@@ -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<Doc>): Promise<Attachment[]> {
|
||||
private async getAttachments (client: TxOperations, objectId: Ref<Doc>): Promise<Attachment[]> {
|
||||
return await client.findAll(attachment.class.Attachment, { attachedTo: objectId })
|
||||
}
|
||||
|
||||
async processMessageEvent (event: AIMessageEventRequest): Promise<void> {
|
||||
if (this.controller.aiClient === undefined) return
|
||||
async processMessageEvent (event: AIEventRequest): Promise<void> {
|
||||
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<Space>) : 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<ChatMessage>)
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
async transferToSupport (event: AITransferEventRequest, channelRef?: Ref<OnboardingChannel>): Promise<void> {
|
||||
// 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<void> {
|
||||
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<OnboardingChannel> | undefined {
|
||||
const key = `${email}-${workspace}`
|
||||
|
||||
return this.channelByKey.get(key)
|
||||
}
|
||||
|
||||
async transfer (event: AITransferEventRequest): Promise<void> {
|
||||
// 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<void> {
|
||||
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<Doc>): Promise<void> {
|
||||
if (tx.objectClass === chunter.class.TypingInfo && this.typingMap.has(tx.objectId)) {
|
||||
this.typingMap.delete(tx.objectId)
|
||||
}
|
||||
}
|
||||
|
||||
protected async txHandler (_: TxOperations, txes: TxCUD<Doc>[]): Promise<void> {
|
||||
private async txHandler (_: TxOperations, txes: TxCUD<Doc>[]): Promise<void> {
|
||||
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<Doc>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openAIChatInSidebar (personId: PersonId): Promise<void> {
|
||||
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<void> {
|
||||
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)
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
for (const [, client] of this.workspaces) {
|
||||
await client.close()
|
||||
|
||||
@@ -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 ?? ''
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
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)
|
||||
// })
|
||||
}
|
||||
|
||||
@@ -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<void>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<OnboardingMessage>
|
||||
): Promise<void> {
|
||||
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<OnboardingMessage>
|
||||
): Promise<void> {
|
||||
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<Person>): Promise<PersonId | undefined> {
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<InlineButton>
|
||||
name: string
|
||||
messageId: Ref<ChatMessage>
|
||||
channelId: Ref<Channel>
|
||||
}
|
||||
|
||||
export enum MessageActions {
|
||||
Accept = 'accept'
|
||||
}
|
||||
|
||||
export interface OnboardingMessage {
|
||||
messageId: Ref<ChatMessage>
|
||||
channelId: Ref<OnboardingChannel>
|
||||
|
||||
@@ -99,7 +99,6 @@ export class Transactor extends DurableObject<Env> {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user