mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Add openai chat (#6411)
This commit is contained in:
parent
a7e3c41b92
commit
4fcc9a435b
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -384,7 +384,8 @@
|
|||||||
"LAST_NAME": "AI",
|
"LAST_NAME": "AI",
|
||||||
"PASSWORD": "password",
|
"PASSWORD": "password",
|
||||||
"AVATAR_PATH": "./assets/avatar.png",
|
"AVATAR_PATH": "./assets/avatar.png",
|
||||||
"AVATAR_CONTENT_TYPE": ".png"
|
"AVATAR_CONTENT_TYPE": ".png",
|
||||||
|
"OPENAI_API_KEY": "token"
|
||||||
},
|
},
|
||||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
|
@ -566,6 +566,9 @@ dependencies:
|
|||||||
'@rush-temp/onboard-resources':
|
'@rush-temp/onboard-resources':
|
||||||
specifier: file:./projects/onboard-resources.tgz
|
specifier: file:./projects/onboard-resources.tgz
|
||||||
version: file:projects/onboard-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(file-loader@6.2.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)(webpack@5.90.3)
|
version: file:projects/onboard-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(file-loader@6.2.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)(webpack@5.90.3)
|
||||||
|
'@rush-temp/openai':
|
||||||
|
specifier: file:./projects/openai.tgz
|
||||||
|
version: file:projects/openai.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)(zod@3.23.8)
|
||||||
'@rush-temp/panel':
|
'@rush-temp/panel':
|
||||||
specifier: file:./projects/panel.tgz
|
specifier: file:./projects/panel.tgz
|
||||||
version: file:projects/panel.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
|
version: file:projects/panel.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
|
||||||
@ -580,7 +583,7 @@ dependencies:
|
|||||||
version: file:projects/pod-account.tgz
|
version: file:projects/pod-account.tgz
|
||||||
'@rush-temp/pod-ai-bot':
|
'@rush-temp/pod-ai-bot':
|
||||||
specifier: file:./projects/pod-ai-bot.tgz
|
specifier: file:./projects/pod-ai-bot.tgz
|
||||||
version: file:projects/pod-ai-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
version: file:projects/pod-ai-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(zod@3.23.8)
|
||||||
'@rush-temp/pod-analytics-collector':
|
'@rush-temp/pod-analytics-collector':
|
||||||
specifier: file:./projects/pod-analytics-collector.tgz
|
specifier: file:./projects/pod-analytics-collector.tgz
|
||||||
version: file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
version: file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||||
@ -1664,6 +1667,9 @@ dependencies:
|
|||||||
octokit:
|
octokit:
|
||||||
specifier: ^3.1.1
|
specifier: ^3.1.1
|
||||||
version: 3.2.1
|
version: 3.2.1
|
||||||
|
openai:
|
||||||
|
specifier: ^4.56.0
|
||||||
|
version: 4.56.0(zod@3.23.8)
|
||||||
otp-generator:
|
otp-generator:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@ -1790,6 +1796,9 @@ dependencies:
|
|||||||
telegram:
|
telegram:
|
||||||
specifier: 2.22.2
|
specifier: 2.22.2
|
||||||
version: 2.22.2
|
version: 2.22.2
|
||||||
|
tiktoken:
|
||||||
|
specifier: ^1.0.16
|
||||||
|
version: 1.0.16
|
||||||
toposort:
|
toposort:
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
@ -1847,6 +1856,9 @@ dependencies:
|
|||||||
ws:
|
ws:
|
||||||
specifier: ^8.18.0
|
specifier: ^8.18.0
|
||||||
version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||||
|
y-indexeddb:
|
||||||
|
specifier: ^9.0.12
|
||||||
|
version: 9.0.12(yjs@13.6.12)
|
||||||
y-prosemirror:
|
y-prosemirror:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.2(prosemirror-model@1.19.4)(y-protocols@1.0.6)(yjs@13.6.12)
|
version: 1.2.2(prosemirror-model@1.19.4)(y-protocols@1.0.6)(yjs@13.6.12)
|
||||||
@ -10283,6 +10295,13 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/agentkeepalive@4.5.0:
|
||||||
|
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
humanize-ms: 1.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/aggregate-error@3.1.0:
|
/aggregate-error@3.1.0:
|
||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -14767,6 +14786,10 @@ packages:
|
|||||||
webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.20.1)(webpack-cli@5.1.4)
|
webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.20.1)(webpack-cli@5.1.4)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/form-data-encoder@1.7.2:
|
||||||
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/form-data@2.5.1:
|
/form-data@2.5.1:
|
||||||
resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==}
|
resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==}
|
||||||
engines: {node: '>= 0.12'}
|
engines: {node: '>= 0.12'}
|
||||||
@ -14785,6 +14808,14 @@ packages:
|
|||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/formdata-node@4.4.1:
|
||||||
|
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||||
|
engines: {node: '>= 12.20'}
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 4.0.0-beta.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/formdata@0.10.4:
|
/formdata@0.10.4:
|
||||||
resolution: {integrity: sha512-IsHa+GYLLXHx0RmpUmzQTdwxDjNinxD+1zKOYPLaRwiqTfex5caQhOzgPIjFgJkL0O884Ers76BSHzXJxHvPLw==}
|
resolution: {integrity: sha512-IsHa+GYLLXHx0RmpUmzQTdwxDjNinxD+1zKOYPLaRwiqTfex5caQhOzgPIjFgJkL0O884Ers76BSHzXJxHvPLw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -15780,6 +15811,12 @@ packages:
|
|||||||
engines: {node: '>=16.17.0'}
|
engines: {node: '>=16.17.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/humanize-ms@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iconv-corefoundation@1.1.7:
|
/iconv-corefoundation@1.1.7:
|
||||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||||
engines: {node: ^8.11.2 || >=10}
|
engines: {node: ^8.11.2 || >=10}
|
||||||
@ -18379,6 +18416,11 @@ packages:
|
|||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-fetch-native@1.6.2:
|
/node-fetch-native@1.6.2:
|
||||||
resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==}
|
resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -18744,6 +18786,27 @@ packages:
|
|||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/openai@4.56.0(zod@3.23.8):
|
||||||
|
resolution: {integrity: sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.17
|
||||||
|
'@types/node-fetch': 2.6.11
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
agentkeepalive: 4.5.0
|
||||||
|
form-data-encoder: 1.7.2
|
||||||
|
formdata-node: 4.4.1
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
zod: 3.23.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
/opener@1.5.2:
|
/opener@1.5.2:
|
||||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -22101,6 +22164,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
|
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tiktoken@1.0.16:
|
||||||
|
resolution: {integrity: sha512-hRcORIGF2YlAgWx3nzrGJOrKSJwLoc81HpXmMQk89632XAgURc7IeV2FgQ2iXo9z/J96fCvpsHg2kWoHcbj9fg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/timm@1.7.1:
|
/timm@1.7.1:
|
||||||
resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==}
|
resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -23061,6 +23128,11 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/web-streams-polyfill@4.0.0-beta.3:
|
||||||
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webidl-conversions@3.0.1:
|
/webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -28877,6 +28949,42 @@ packages:
|
|||||||
- ts-node
|
- ts-node
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
file:projects/openai.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)(zod@3.23.8):
|
||||||
|
resolution: {integrity: sha512-jWsGOjlco80/7CIZ2Ogs7lCWr23lHUDQIB2hkPgYu2rbvM7iwSz5jhPjTPmR2y9gkVTSyyd9ozTo9LuxPpI6Kg==, tarball: file:projects/openai.tgz}
|
||||||
|
id: file:projects/openai.tgz
|
||||||
|
name: '@rush-temp/openai'
|
||||||
|
version: 0.0.0
|
||||||
|
dependencies:
|
||||||
|
'@types/jest': 29.5.12
|
||||||
|
'@types/node': 20.11.19
|
||||||
|
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
eslint: 8.56.0
|
||||||
|
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||||
|
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||||
|
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||||
|
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||||
|
openai: 4.56.0(zod@3.23.8)
|
||||||
|
prettier: 3.2.5
|
||||||
|
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||||
|
tiktoken: 1.0.16
|
||||||
|
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||||
|
typescript: 5.3.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@babel/core'
|
||||||
|
- '@jest/types'
|
||||||
|
- babel-jest
|
||||||
|
- babel-plugin-macros
|
||||||
|
- encoding
|
||||||
|
- esbuild
|
||||||
|
- node-notifier
|
||||||
|
- supports-color
|
||||||
|
- svelte
|
||||||
|
- ts-node
|
||||||
|
- zod
|
||||||
|
dev: false
|
||||||
|
|
||||||
file:projects/panel.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
file:projects/panel.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||||
resolution: {integrity: sha512-1v1IuSTwDSjPgaZsKWG4tEUINvY3KtFNYXDHvUVkefqJm280nZg5G4QnphrKdOseOxHN7zaSq5CoVxURn1hV2g==, tarball: file:projects/panel.tgz}
|
resolution: {integrity: sha512-1v1IuSTwDSjPgaZsKWG4tEUINvY3KtFNYXDHvUVkefqJm280nZg5G4QnphrKdOseOxHN7zaSq5CoVxURn1hV2g==, tarball: file:projects/panel.tgz}
|
||||||
id: file:projects/panel.tgz
|
id: file:projects/panel.tgz
|
||||||
@ -29026,8 +29134,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/pod-ai-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
file:projects/pod-ai-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(zod@3.23.8):
|
||||||
resolution: {integrity: sha512-5Upjw52FvxxuZY9eSN8N5FUKoNMhqME7DwEAp4TTQEKR/Rs+Fnxi7S5ZQ6G2G40RmZ0F0mWfXfWTcyhFlyio5Q==, tarball: file:projects/pod-ai-bot.tgz}
|
resolution: {integrity: sha512-/UB67RfT0fI4XW6vu6sYFLs57ciAWNBNDq5Au/5qgN1ilwh/VvgqJNgwiGTPkqt4b1Q9AKfpy7AIRy2FoAk1/A==, tarball: file:projects/pod-ai-bot.tgz}
|
||||||
id: file:projects/pod-ai-bot.tgz
|
id: file:projects/pod-ai-bot.tgz
|
||||||
name: '@rush-temp/pod-ai-bot'
|
name: '@rush-temp/pod-ai-bot'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -29056,7 +29164,9 @@ packages:
|
|||||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||||
mongodb: 6.8.0
|
mongodb: 6.8.0
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
|
openai: 4.56.0(zod@3.23.8)
|
||||||
prettier: 3.2.5
|
prettier: 3.2.5
|
||||||
|
tiktoken: 1.0.16
|
||||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||||
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
||||||
typescript: 5.3.3
|
typescript: 5.3.3
|
||||||
@ -29080,6 +29190,7 @@ packages:
|
|||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
- zod
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
||||||
@ -30035,7 +30146,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
|
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
|
||||||
resolution: {integrity: sha512-lYEzH4pmYid/iB0WNGOZXt3y9nyDbs70r3jYZJ6qIQEyvQVqWXnAd8Rw+nqcEbI/QtTou81FrvAK2Fnp5Imeww==, tarball: file:projects/prod.tgz}
|
resolution: {integrity: sha512-BKzsLd9ugM4rEyUkJyeMG3QjOS0RLA/ipkLIIRC+NUUQi/zMuHMEumvoSok539kpPsVgRrtJdnDBa+3tja5fSA==, tarball: file:projects/prod.tgz}
|
||||||
id: file:projects/prod.tgz
|
id: file:projects/prod.tgz
|
||||||
name: '@rush-temp/prod'
|
name: '@rush-temp/prod'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -30932,6 +31043,7 @@ packages:
|
|||||||
'@types/node': 20.11.19
|
'@types/node': 20.11.19
|
||||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
bson: 6.8.0
|
||||||
eslint: 8.56.0
|
eslint: 8.56.0
|
||||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||||
|
@ -45,7 +45,7 @@ export class TAIBotEvent extends TDoc implements AIBotEvent {
|
|||||||
message!: string
|
message!: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(aiBot.class.AIBotResponseEvent, core.class.Doc)
|
@Model(aiBot.class.AIBotResponseEvent, aiBot.class.AIBotEvent)
|
||||||
export class TAIBotResponseEvent extends TAIBotEvent implements AIBotResponseEvent {
|
export class TAIBotResponseEvent extends TAIBotEvent implements AIBotResponseEvent {
|
||||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
||||||
objectId!: Ref<Doc>
|
objectId!: Ref<Doc>
|
||||||
|
7
plugins/openai/.eslintrc.js
Normal file
7
plugins/openai/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
}
|
4
plugins/openai/.npmignore
Normal file
4
plugins/openai/.npmignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!/lib/**
|
||||||
|
!CHANGELOG.md
|
||||||
|
/lib/**/__tests__/
|
4
plugins/openai/config/rig.json
Normal file
4
plugins/openai/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||||
|
"rigPackageName": "@hcengineering/platform-rig"
|
||||||
|
}
|
7
plugins/openai/jest.config.js
Normal file
7
plugins/openai/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||||
|
roots: ["./src"],
|
||||||
|
coverageReporters: ["text-summary", "html"]
|
||||||
|
}
|
47
plugins/openai/package.json
Normal file
47
plugins/openai/package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@hcengineering/openai",
|
||||||
|
"version": "0.6.0",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"svelte": "src/index.ts",
|
||||||
|
"types": "types/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"lib/**/*",
|
||||||
|
"types/**/*",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"author": "Copyright © Hardcore Engineering Inc.",
|
||||||
|
"license": "EPL-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "compile",
|
||||||
|
"build:watch": "compile",
|
||||||
|
"test": "jest --passWithNoTests --silent",
|
||||||
|
"format": "format src",
|
||||||
|
"_phase:build": "compile transpile src",
|
||||||
|
"_phase:test": "jest --passWithNoTests --silent",
|
||||||
|
"_phase:format": "format src",
|
||||||
|
"_phase:validate": "compile validate"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@hcengineering/platform-rig": "^0.6.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-n": "^15.4.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
|
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"@types/jest": "^29.5.5",
|
||||||
|
"prettier-plugin-svelte": "^3.2.2",
|
||||||
|
"@types/node": "~20.11.16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hcengineering/core": "^0.6.32",
|
||||||
|
"@hcengineering/platform": "^0.6.11",
|
||||||
|
"openai": "^4.56.0",
|
||||||
|
"tiktoken": "^1.0.16"
|
||||||
|
}
|
||||||
|
}
|
@ -13,14 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Metadata, type Plugin, plugin } from '@hcengineering/platform'
|
import type { Plugin } from '@hcengineering/platform'
|
||||||
|
import { plugin } from '@hcengineering/platform'
|
||||||
|
|
||||||
export const analyticsCollectorId = 'analyticsCollector' as Plugin
|
export const openaiId = 'openai' as Plugin
|
||||||
|
|
||||||
export const analyticsCollector = plugin(analyticsCollectorId, {
|
export * from './utils'
|
||||||
metadata: {
|
|
||||||
EndpointURL: '' as Metadata<string>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default analyticsCollector
|
const openai = plugin(openaiId, {})
|
||||||
|
|
||||||
|
export default openai
|
41
plugins/openai/src/utils.ts
Normal file
41
plugins/openai/src/utils.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
import { Tiktoken } from 'tiktoken'
|
||||||
|
|
||||||
|
// Based on https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||||
|
export function countTokens (messages: OpenAI.ChatCompletionMessageParam[], encoding: Tiktoken): number {
|
||||||
|
const tokensPerMessage = 3 // every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||||
|
const tokensPerName = 1 // every name follows <|name|>{name}<|end|>\n
|
||||||
|
|
||||||
|
let result = 0
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
result += tokensPerMessage
|
||||||
|
for (const key in message) {
|
||||||
|
const value = message[key as keyof OpenAI.ChatCompletionMessageParam] as string
|
||||||
|
if (value == null) continue
|
||||||
|
result += encoding.encode(value).length
|
||||||
|
if (key === 'name') {
|
||||||
|
result += tokensPerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result += 3 // every reply is primed with <|start|>assistant<|message|>
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
10
plugins/openai/tsconfig.json
Normal file
10
plugins/openai/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"declarationDir": "./types",
|
||||||
|
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
@ -2121,6 +2121,11 @@
|
|||||||
"packageName": "@hcengineering/pod-telegram-bot",
|
"packageName": "@hcengineering/pod-telegram-bot",
|
||||||
"projectFolder": "services/telegram-bot/pod-telegram-bot",
|
"projectFolder": "services/telegram-bot/pod-telegram-bot",
|
||||||
"shouldPublish": false
|
"shouldPublish": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@hcengineering/openai",
|
||||||
|
"projectFolder": "plugins/openai",
|
||||||
|
"shouldPublish": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -31,11 +31,11 @@ import core, {
|
|||||||
import { TriggerControl } from '@hcengineering/server-core'
|
import { TriggerControl } from '@hcengineering/server-core'
|
||||||
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||||
import aiBot, { aiBotAccountEmail, AIBotResponseEvent } from '@hcengineering/ai-bot'
|
import aiBot, { aiBotAccountEmail, AIBotResponseEvent } from '@hcengineering/ai-bot'
|
||||||
import serverAIBot, { AIBotServiceAdapter, serverAiBotId } from '@hcengineering/server-ai-bot'
|
import { AIBotServiceAdapter, serverAiBotId } from '@hcengineering/server-ai-bot'
|
||||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||||
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
|
||||||
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||||
|
import { getSupportWorkspaceId } from './utils'
|
||||||
|
|
||||||
async function processWorkspace (control: TriggerControl): Promise<void> {
|
async function processWorkspace (control: TriggerControl): Promise<void> {
|
||||||
const adapter = control.serviceAdaptersManager.getAdapter(serverAiBotId) as AIBotServiceAdapter | undefined
|
const adapter = control.serviceAdaptersManager.getAdapter(serverAiBotId) as AIBotServiceAdapter | undefined
|
||||||
@ -62,14 +62,6 @@ async function isDirectAvailable (direct: DirectMessage, control: TriggerControl
|
|||||||
return persons.size === 2
|
return persons.size === 2
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isAvailableDoc (doc: Doc, control: TriggerControl): Promise<boolean> {
|
|
||||||
if (doc._class === chunter.class.DirectMessage) {
|
|
||||||
return await isDirectAvailable(doc as DirectMessage, control)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise<Doc | undefined> {
|
async function getMessageDoc (message: ChatMessage, control: TriggerControl): Promise<Doc | undefined> {
|
||||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
const thread = message as ThreadMessage
|
const thread = message as ThreadMessage
|
||||||
@ -85,15 +77,7 @@ async function getMessageDoc (message: ChatMessage, control: TriggerControl): Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessageData (
|
function getMessageData (doc: Doc, message: ChatMessage): Data<AIBotResponseEvent> {
|
||||||
doc: Doc,
|
|
||||||
message: ChatMessage,
|
|
||||||
control: TriggerControl
|
|
||||||
): Promise<Data<AIBotResponseEvent> | undefined> {
|
|
||||||
if (!(await isAvailableDoc(doc, control))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectId: message.attachedTo,
|
objectId: message.attachedTo,
|
||||||
objectClass: message.attachedToClass,
|
objectClass: message.attachedToClass,
|
||||||
@ -105,15 +89,7 @@ async function getMessageData (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getThreadMessageData (
|
function getThreadMessageData (message: ThreadMessage): Data<AIBotResponseEvent> {
|
||||||
doc: Doc,
|
|
||||||
message: ThreadMessage,
|
|
||||||
control: TriggerControl
|
|
||||||
): Promise<Data<AIBotResponseEvent> | undefined> {
|
|
||||||
if (!(await isAvailableDoc(doc, control))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectId: message.attachedTo,
|
objectId: message.attachedTo,
|
||||||
objectClass: message.attachedToClass,
|
objectClass: message.attachedToClass,
|
||||||
@ -125,33 +101,14 @@ async function getThreadMessageData (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: temporally commented until open ai will be added
|
async function createResponseEvent (
|
||||||
// async function createAIBotEvent (message: ChatMessage, control: TriggerControl): Promise<Tx[]> {
|
message: ChatMessage,
|
||||||
// const doc = await getMessageDoc(message, control)
|
control: TriggerControl,
|
||||||
//
|
data: Data<AIBotResponseEvent>
|
||||||
// if (doc === undefined) {
|
): Promise<void> {
|
||||||
// return []
|
const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotResponseEvent, message.space, data)
|
||||||
// }
|
await control.apply([eventTx])
|
||||||
//
|
}
|
||||||
// let data: Data<AIBotResponseEvent> | undefined
|
|
||||||
//
|
|
||||||
// if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
|
||||||
// data = await getThreadMessageData(doc, message as ThreadMessage, control)
|
|
||||||
// } else {
|
|
||||||
// data = await getMessageData(doc, message, control)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (data === undefined) {
|
|
||||||
// return []
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotResponseEvent, message.space, data)
|
|
||||||
//
|
|
||||||
// await processWorkspace(control)
|
|
||||||
//
|
|
||||||
// return [eventTx]
|
|
||||||
// return []
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise<Ref<ChatMessage> | undefined> {
|
async function getThreadParent (control: TriggerControl, message: ChatMessage): Promise<Ref<ChatMessage> | undefined> {
|
||||||
if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) {
|
if (!control.hierarchy.isDerived(message.attachedToClass, chunter.class.ChatMessage)) {
|
||||||
@ -172,28 +129,44 @@ async function getThreadParent (control: TriggerControl, message: ChatMessage):
|
|||||||
return message.attachedTo as Ref<ChatMessage>
|
return message.attachedTo as Ref<ChatMessage>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSupportWorkspaceId (): string | undefined {
|
async function createTransferEvent (
|
||||||
const supportWorkspaceId = getMetadata(serverAIBot.metadata.SupportWorkspaceId)
|
control: TriggerControl,
|
||||||
|
message: ChatMessage,
|
||||||
if (supportWorkspaceId === '') {
|
account: PersonAccount,
|
||||||
return undefined
|
data: Data<AIBotResponseEvent>
|
||||||
|
): Promise<void> {
|
||||||
|
if (account.role !== AccountRole.Owner) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return supportWorkspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise<void> {
|
|
||||||
const supportWorkspaceId = getSupportWorkspaceId()
|
const supportWorkspaceId = getSupportWorkspaceId()
|
||||||
|
|
||||||
if (supportWorkspaceId === undefined) {
|
if (supportWorkspaceId === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
||||||
|
messageClass: data.messageClass,
|
||||||
|
message: message.message,
|
||||||
|
collection: data.collection,
|
||||||
|
toWorkspace: supportWorkspaceId,
|
||||||
|
toEmail: account.email,
|
||||||
|
fromWorkspace: toWorkspaceString(control.workspace),
|
||||||
|
fromWorkspaceName: control.workspace.workspaceName,
|
||||||
|
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
||||||
|
messageId: message._id,
|
||||||
|
parentMessageId: await getThreadParent(control, message)
|
||||||
|
})
|
||||||
|
|
||||||
|
await control.apply([eventTx])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise<void> {
|
||||||
const account = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
const account = control.modelDb.findAllSync(contact.class.PersonAccount, {
|
||||||
_id: (message.createdBy ?? message.modifiedBy) as Ref<PersonAccount>
|
_id: (message.createdBy ?? message.modifiedBy) as Ref<PersonAccount>
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
if (account === undefined || account.role !== AccountRole.Owner) {
|
if (account === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,29 +185,14 @@ async function onBotDirectMessageSend (control: TriggerControl, message: ChatMes
|
|||||||
let data: Data<AIBotResponseEvent> | undefined
|
let data: Data<AIBotResponseEvent> | undefined
|
||||||
|
|
||||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
data = await getThreadMessageData(direct, message as ThreadMessage, control)
|
data = getThreadMessageData(message as ThreadMessage)
|
||||||
} else {
|
} else {
|
||||||
data = await getMessageData(direct, message, control)
|
data = getMessageData(direct, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data === undefined) {
|
await createResponseEvent(message, control, data)
|
||||||
return
|
await createTransferEvent(control, message, account, data)
|
||||||
}
|
|
||||||
|
|
||||||
const eventTx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
|
||||||
messageClass: data.messageClass,
|
|
||||||
message: message.message,
|
|
||||||
collection: data.collection,
|
|
||||||
toWorkspace: supportWorkspaceId,
|
|
||||||
toEmail: account.email,
|
|
||||||
fromWorkspace: toWorkspaceString(control.workspace),
|
|
||||||
fromWorkspaceName: control.workspace.workspaceName,
|
|
||||||
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
|
||||||
messageId: message._id,
|
|
||||||
parentMessageId: await getThreadParent(control, message)
|
|
||||||
})
|
|
||||||
|
|
||||||
await control.apply([eventTx])
|
|
||||||
await processWorkspace(control)
|
await processWorkspace(control)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,13 +221,9 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
|||||||
let data: Data<AIBotResponseEvent> | undefined
|
let data: Data<AIBotResponseEvent> | undefined
|
||||||
|
|
||||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
data = await getThreadMessageData(channel, message as ThreadMessage, control)
|
data = getThreadMessageData(message as ThreadMessage)
|
||||||
} else {
|
} else {
|
||||||
data = await getMessageData(channel, message, control)
|
data = getMessageData(channel, message)
|
||||||
}
|
|
||||||
|
|
||||||
if (data === undefined) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
const tx = control.txFactory.createTxCreateDoc(aiBot.class.AIBotTransferEvent, message.space, {
|
||||||
@ -348,7 +302,7 @@ export async function OnMention (tx: TxCreateDoc<MentionInboxNotification>, cont
|
|||||||
// return []
|
// return []
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// await createAIBotEvent(message, control)
|
// await createResponseEvent(message, control)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -387,7 +341,7 @@ export async function OnMessageNotified (
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// if (isDocMentioned(personAccount.person, message.message)) {
|
// if (isDocMentioned(personAccount.person, message.message)) {
|
||||||
// return await createAIBotEvent(message, control)
|
// return await createResponseEvent(message, control)
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// if (!control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
// if (!control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||||
@ -403,7 +357,7 @@ export async function OnMessageNotified (
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// if (parent.createdBy === aiBot.account.AIBot) {
|
// if (parent.createdBy === aiBot.account.AIBot) {
|
||||||
// return await createAIBotEvent(message, control)
|
// return await createResponseEvent(message, control)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
27
server-plugins/ai-bot-resources/src/utils.ts
Normal file
27
server-plugins/ai-bot-resources/src/utils.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// 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 { getMetadata } from '@hcengineering/platform'
|
||||||
|
import serverAIBot from '@hcengineering/server-ai-bot'
|
||||||
|
|
||||||
|
export function getSupportWorkspaceId (): string | undefined {
|
||||||
|
const supportWorkspaceId = getMetadata(serverAIBot.metadata.SupportWorkspaceId)
|
||||||
|
|
||||||
|
if (supportWorkspaceId === '') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportWorkspaceId
|
||||||
|
}
|
@ -64,6 +64,7 @@
|
|||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
"@hcengineering/mongo": "^0.6.1",
|
"@hcengineering/mongo": "^0.6.1",
|
||||||
"@hcengineering/notification": "^0.6.23",
|
"@hcengineering/notification": "^0.6.23",
|
||||||
|
"@hcengineering/openai": "^0.6.0",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/server-ai-bot": "^0.6.0",
|
"@hcengineering/server-ai-bot": "^0.6.0",
|
||||||
"@hcengineering/server-analytics-collector-resources": "^0.6.0",
|
"@hcengineering/server-analytics-collector-resources": "^0.6.0",
|
||||||
@ -72,6 +73,7 @@
|
|||||||
"@hcengineering/server-token": "^0.6.11",
|
"@hcengineering/server-token": "^0.6.11",
|
||||||
"@hcengineering/server-ws": "^0.6.11",
|
"@hcengineering/server-ws": "^0.6.11",
|
||||||
"@hcengineering/setting": "^0.6.17",
|
"@hcengineering/setting": "^0.6.17",
|
||||||
|
"@hcengineering/text": "^0.6.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "~16.0.0",
|
"dotenv": "~16.0.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
@ -79,6 +81,8 @@
|
|||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"mongodb": "^6.8.0",
|
"mongodb": "^6.8.0",
|
||||||
"node-fetch": "^2.6.6",
|
"node-fetch": "^2.6.6",
|
||||||
|
"openai": "^4.56.0",
|
||||||
|
"tiktoken": "^1.0.16",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
import { LoginInfo, Workspace, WorkspaceLoginInfo } from '@hcengineering/account'
|
import { LoginInfo, Workspace, WorkspaceLoginInfo } from '@hcengineering/account'
|
||||||
import aiBot, { aiBotAccountEmail } from '@hcengineering/ai-bot'
|
import aiBot, { aiBotAccountEmail } from '@hcengineering/ai-bot'
|
||||||
import { AccountRole, WorkspaceId } from '@hcengineering/core'
|
import { AccountRole } from '@hcengineering/core'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
|
|
||||||
export async function assignBotToWorkspace (workspaceId: WorkspaceId): Promise<Workspace> {
|
export async function assignBotToWorkspace (workspace: string): Promise<Workspace> {
|
||||||
const accountsUrl = config.AccountsURL
|
const accountsUrl = config.AccountsURL
|
||||||
const workspace = await (
|
const res = await (
|
||||||
await fetch(accountsUrl, {
|
await fetch(accountsUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -29,20 +29,12 @@ export async function assignBotToWorkspace (workspaceId: WorkspaceId): Promise<W
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
method: 'assignWorkspace',
|
method: 'assignWorkspace',
|
||||||
params: [
|
params: [aiBotAccountEmail, workspace, AccountRole.User, undefined, false, undefined, aiBot.account.AIBot]
|
||||||
aiBotAccountEmail,
|
|
||||||
workspaceId.name,
|
|
||||||
AccountRole.User,
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
aiBot.account.AIBot
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
return workspace.result as Workspace
|
return res.result as Workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBotAccount (): Promise<Workspace> {
|
export async function createBotAccount (): Promise<Workspace> {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
AccountsURL: string
|
AccountsURL: string
|
||||||
ConfigurationDB: string
|
ConfigurationDB: string
|
||||||
@ -26,6 +28,10 @@ interface Config {
|
|||||||
AvatarName: string
|
AvatarName: string
|
||||||
AvatarContentType: string
|
AvatarContentType: string
|
||||||
Password: string
|
Password: string
|
||||||
|
OpenAIKey: string
|
||||||
|
OpenAIModel: OpenAI.ChatModel
|
||||||
|
MaxContentTokens: number
|
||||||
|
MaxHistoryRecords: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const envMap: { [key in keyof Config]: string } = {
|
const envMap: { [key in keyof Config]: string } = {
|
||||||
@ -40,9 +46,15 @@ const envMap: { [key in keyof Config]: string } = {
|
|||||||
AvatarPath: 'AVATAR_PATH',
|
AvatarPath: 'AVATAR_PATH',
|
||||||
AvatarName: 'AVATAR_NAME',
|
AvatarName: 'AVATAR_NAME',
|
||||||
AvatarContentType: 'AVATAR_CONTENT_TYPE',
|
AvatarContentType: 'AVATAR_CONTENT_TYPE',
|
||||||
Password: 'PASSWORD'
|
Password: 'PASSWORD',
|
||||||
|
OpenAIKey: 'OPENAI_API_KEY',
|
||||||
|
OpenAIModel: 'OPENAI_MODEL',
|
||||||
|
MaxContentTokens: 'MAX_CONTENT_TOKENS',
|
||||||
|
MaxHistoryRecords: 'MAX_HISTORY_RECORDS'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||||
|
|
||||||
const config: Config = (() => {
|
const config: Config = (() => {
|
||||||
const params: Partial<Config> = {
|
const params: Partial<Config> = {
|
||||||
AccountsURL: process.env[envMap.AccountsURL],
|
AccountsURL: process.env[envMap.AccountsURL],
|
||||||
@ -56,7 +68,11 @@ const config: Config = (() => {
|
|||||||
AvatarPath: process.env[envMap.AvatarPath] ?? './assets/avatar.png',
|
AvatarPath: process.env[envMap.AvatarPath] ?? './assets/avatar.png',
|
||||||
AvatarName: process.env[envMap.AvatarName] ?? 'huly_ai_bot_avatar',
|
AvatarName: process.env[envMap.AvatarName] ?? 'huly_ai_bot_avatar',
|
||||||
AvatarContentType: process.env[envMap.AvatarContentType] ?? '.png',
|
AvatarContentType: process.env[envMap.AvatarContentType] ?? '.png',
|
||||||
Password: process.env[envMap.Password] ?? 'password'
|
Password: process.env[envMap.Password] ?? 'password',
|
||||||
|
OpenAIKey: process.env[envMap.OpenAIKey],
|
||||||
|
OpenAIModel: (process.env[envMap.OpenAIModel] ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
||||||
|
MaxContentTokens: parseNumber(process.env[envMap.MaxContentTokens]) ?? 128 * 100,
|
||||||
|
MaxHistoryRecords: parseNumber(process.env[envMap.MaxHistoryRecords]) ?? 500
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingEnv = (Object.keys(params) as Array<keyof Config>)
|
const missingEnv = (Object.keys(params) as Array<keyof Config>)
|
||||||
|
@ -13,16 +13,19 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Db, Collection } from 'mongodb'
|
import { MeasureContext, systemAccountEmail } from '@hcengineering/core'
|
||||||
import { getWorkspaceId, MeasureContext, systemAccountEmail, toWorkspaceString, WorkspaceId } from '@hcengineering/core'
|
|
||||||
import { aiBotAccountEmail, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
import { aiBotAccountEmail, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
||||||
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
import { getTransactorEndpoint } from '@hcengineering/server-client'
|
import { getTransactorEndpoint } from '@hcengineering/server-client'
|
||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
import { WorkspaceLoginInfo } from '@hcengineering/account'
|
import { WorkspaceLoginInfo } from '@hcengineering/account'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
import { encoding_for_model } from 'tiktoken'
|
||||||
|
|
||||||
import { WorkspaceClient } from './workspaceClient'
|
import { WorkspaceClient } from './workspaceClient'
|
||||||
import { assignBotToWorkspace, getWorkspaceInfo } from './account'
|
import { assignBotToWorkspace, getWorkspaceInfo } from './account'
|
||||||
|
import config from './config'
|
||||||
|
import { DbStorage } from './storage'
|
||||||
|
|
||||||
const POLLING_INTERVAL_MS = 5 * 1000 // 5 seconds
|
const POLLING_INTERVAL_MS = 5 * 1000 // 5 seconds
|
||||||
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
||||||
@ -34,19 +37,19 @@ export class AIBotController {
|
|||||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||||
private readonly connectingWorkspaces: Set<string> = new Set<string>()
|
private readonly connectingWorkspaces: Set<string> = new Set<string>()
|
||||||
|
|
||||||
private readonly db: Db
|
|
||||||
private readonly ctx: MeasureContext
|
|
||||||
private readonly workspacesInfoCollection: Collection<WorkspaceInfoRecord>
|
|
||||||
|
|
||||||
private readonly intervalId: NodeJS.Timeout
|
private readonly intervalId: NodeJS.Timeout
|
||||||
|
|
||||||
|
readonly aiClient: OpenAI
|
||||||
|
readonly encoding = encoding_for_model(config.OpenAIModel)
|
||||||
|
|
||||||
assignTimeout: NodeJS.Timeout | undefined
|
assignTimeout: NodeJS.Timeout | undefined
|
||||||
assignAttempts = 0
|
assignAttempts = 0
|
||||||
|
|
||||||
constructor (mongoDb: Db, ctx: MeasureContext) {
|
constructor (
|
||||||
this.db = mongoDb
|
readonly storage: DbStorage,
|
||||||
this.ctx = ctx
|
private readonly ctx: MeasureContext
|
||||||
this.workspacesInfoCollection = this.db.collection<WorkspaceInfoRecord>('workspacesInfo')
|
) {
|
||||||
|
this.aiClient = new OpenAI({ apiKey: config.OpenAIKey })
|
||||||
|
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
void this.updateWorkspaceClients()
|
void this.updateWorkspaceClients()
|
||||||
@ -54,12 +57,10 @@ export class AIBotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceClients (): Promise<void> {
|
async updateWorkspaceClients (): Promise<void> {
|
||||||
const activeRecords = await this.workspacesInfoCollection.find({ active: true }).toArray()
|
const activeRecords = await this.storage.getActiveWorkspaces()
|
||||||
|
|
||||||
for (const record of activeRecords) {
|
for (const record of activeRecords) {
|
||||||
const id: WorkspaceId = { name: record.workspace }
|
const ws = record.workspace
|
||||||
|
|
||||||
const ws = toWorkspaceString(id)
|
|
||||||
|
|
||||||
if (this.workspaces.has(ws)) {
|
if (this.workspaces.has(ws)) {
|
||||||
continue
|
continue
|
||||||
@ -69,13 +70,11 @@ export class AIBotController {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.initWorkspaceClient(id, record)
|
await this.initWorkspaceClient(ws, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeWorkspaceClient (workspaceId: WorkspaceId): Promise<void> {
|
async closeWorkspaceClient (workspace: string): Promise<void> {
|
||||||
const workspace = toWorkspaceString(workspaceId)
|
|
||||||
|
|
||||||
this.ctx.info('Closing workspace client: ', { workspace })
|
this.ctx.info('Closing workspace client: ', { workspace })
|
||||||
|
|
||||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||||
@ -85,7 +84,7 @@ export class AIBotController {
|
|||||||
this.closeWorkspaceTimeouts.delete(workspace)
|
this.closeWorkspaceTimeouts.delete(workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspacesInfoCollection.updateOne({ workspace: workspaceId.name }, { $set: { active: false } })
|
await this.storage.inactiveWorkspace(workspace)
|
||||||
|
|
||||||
const client = this.workspaces.get(workspace)
|
const client = this.workspaces.get(workspace)
|
||||||
|
|
||||||
@ -96,14 +95,14 @@ export class AIBotController {
|
|||||||
this.connectingWorkspaces.delete(workspace)
|
this.connectingWorkspaces.delete(workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getWorkspaceInfo (ws: WorkspaceId): Promise<WorkspaceLoginInfo | undefined> {
|
private async getWorkspaceInfo (ws: string): Promise<WorkspaceLoginInfo | undefined> {
|
||||||
const systemToken = generateToken(systemAccountEmail, ws)
|
const systemToken = generateToken(systemAccountEmail, { name: ws })
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
try {
|
try {
|
||||||
const info = await getWorkspaceInfo(systemToken)
|
const info = await getWorkspaceInfo(systemToken)
|
||||||
|
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
this.ctx.warn('Cannot find workspace info', ws)
|
this.ctx.warn('Cannot find workspace info', { workspace: ws })
|
||||||
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
await wait(ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -116,51 +115,50 @@ export class AIBotController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async assignToWorkspace (ws: WorkspaceId): Promise<void> {
|
private async assignToWorkspace (workspace: string): Promise<void> {
|
||||||
clearTimeout(this.assignTimeout)
|
clearTimeout(this.assignTimeout)
|
||||||
try {
|
try {
|
||||||
const info = await this.getWorkspaceInfo(ws)
|
const info = await this.getWorkspaceInfo(workspace)
|
||||||
|
|
||||||
if (info === undefined) {
|
if (info === undefined) {
|
||||||
void this.closeWorkspaceClient(ws)
|
void this.closeWorkspaceClient(workspace)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.creating === true) {
|
if (info.creating === true) {
|
||||||
this.ctx.info('Workspace is creating -> waiting...', ws)
|
this.ctx.info('Workspace is creating -> waiting...', { workspace })
|
||||||
this.assignTimeout = setTimeout(() => {
|
this.assignTimeout = setTimeout(() => {
|
||||||
void this.assignToWorkspace(ws)
|
void this.assignToWorkspace(workspace)
|
||||||
}, ASSIGN_WORKSPACE_DELAY_MS)
|
}, ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await assignBotToWorkspace(ws)
|
const result = await assignBotToWorkspace(workspace)
|
||||||
this.ctx.info('Assign to workspace result: ', { result, workspace: ws.name })
|
this.ctx.info('Assign to workspace result: ', { result, workspace })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.ctx.error('Error during assign workspace:', { e })
|
this.ctx.error('Error during assign workspace:', { e })
|
||||||
if (this.assignAttempts < MAX_ASSIGN_ATTEMPTS) {
|
if (this.assignAttempts < MAX_ASSIGN_ATTEMPTS) {
|
||||||
this.assignAttempts++
|
this.assignAttempts++
|
||||||
this.assignTimeout = setTimeout(() => {
|
this.assignTimeout = setTimeout(() => {
|
||||||
void this.assignToWorkspace(ws)
|
void this.assignToWorkspace(workspace)
|
||||||
}, ASSIGN_WORKSPACE_DELAY_MS)
|
}, ASSIGN_WORKSPACE_DELAY_MS)
|
||||||
} else {
|
} else {
|
||||||
void this.closeWorkspaceClient(ws)
|
void this.closeWorkspaceClient(workspace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initWorkspaceClient (workspaceId: WorkspaceId, info: WorkspaceInfoRecord): Promise<void> {
|
async initWorkspaceClient (workspace: string, info: WorkspaceInfoRecord): Promise<void> {
|
||||||
const workspace = toWorkspaceString(workspaceId)
|
|
||||||
this.connectingWorkspaces.add(workspace)
|
this.connectingWorkspaces.add(workspace)
|
||||||
|
|
||||||
if (!this.workspaces.has(workspace)) {
|
if (!this.workspaces.has(workspace)) {
|
||||||
this.ctx.info('Listen workspace: ', { workspace })
|
this.ctx.info('Listen workspace: ', { workspace })
|
||||||
await this.assignToWorkspace(workspaceId)
|
await this.assignToWorkspace(workspace)
|
||||||
const token = generateToken(aiBotAccountEmail, workspaceId)
|
const token = generateToken(aiBotAccountEmail, { name: workspace })
|
||||||
const endpoint = await getTransactorEndpoint(token)
|
const endpoint = await getTransactorEndpoint(token)
|
||||||
this.workspaces.set(
|
this.workspaces.set(
|
||||||
workspace,
|
workspace,
|
||||||
new WorkspaceClient(endpoint, token, workspaceId, this, this.ctx.newChild(workspace, {}), info)
|
new WorkspaceClient(endpoint, token, workspace, this, this.ctx.newChild(workspace, {}), info)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +169,7 @@ export class AIBotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newTimeoutId = setTimeout(() => {
|
const newTimeoutId = setTimeout(() => {
|
||||||
void this.closeWorkspaceClient(workspaceId)
|
void this.closeWorkspaceClient(workspace)
|
||||||
}, CLOSE_INTERVAL_MS)
|
}, CLOSE_INTERVAL_MS)
|
||||||
|
|
||||||
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
|
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
|
||||||
@ -179,12 +177,17 @@ export class AIBotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async transfer (event: AIBotTransferEvent): Promise<void> {
|
async transfer (event: AIBotTransferEvent): Promise<void> {
|
||||||
const workspaceId = getWorkspaceId(event.toWorkspace)
|
const workspace = event.toWorkspace
|
||||||
const info = await this.workspacesInfoCollection.find({ workspace: workspaceId.name }).toArray()
|
const info = await this.storage.getWorkspace(workspace)
|
||||||
|
|
||||||
await this.initWorkspaceClient(workspaceId, info[0])
|
if (info === undefined) {
|
||||||
|
this.ctx.error('Workspace info not found -> cannot transfer event', { workspace })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const wsClient = this.workspaces.get(event.toWorkspace)
|
await this.initWorkspaceClient(workspace, info)
|
||||||
|
|
||||||
|
const wsClient = this.workspaces.get(workspace)
|
||||||
|
|
||||||
if (wsClient === undefined) {
|
if (wsClient === undefined) {
|
||||||
return
|
return
|
||||||
@ -195,6 +198,9 @@ export class AIBotController {
|
|||||||
|
|
||||||
async close (): Promise<void> {
|
async close (): Promise<void> {
|
||||||
clearInterval(this.intervalId)
|
clearInterval(this.intervalId)
|
||||||
|
|
||||||
|
this.encoding.free()
|
||||||
|
|
||||||
for (const workspace of this.workspaces.values()) {
|
for (const workspace of this.workspaces.values()) {
|
||||||
await workspace.close()
|
await workspace.close()
|
||||||
}
|
}
|
||||||
@ -204,11 +210,8 @@ export class AIBotController {
|
|||||||
this.workspaces.clear()
|
this.workspaces.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAvatarInfo (workspace: WorkspaceId, path: string, lastModified: number): Promise<void> {
|
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
|
||||||
await this.workspacesInfoCollection.updateOne(
|
await this.storage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } })
|
||||||
{ workspace: workspace.name },
|
|
||||||
{ $set: { avatarPath: path, avatarLastModified: lastModified } }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import { MeasureMetricsContext } from '@hcengineering/core'
|
|||||||
import serverClient from '@hcengineering/server-client'
|
import serverClient from '@hcengineering/server-client'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { closeDB, getDB } from './storage'
|
import { closeDB, DbStorage, getDB } from './storage'
|
||||||
import { AIBotController } from './controller'
|
import { AIBotController } from './controller'
|
||||||
import { createBotAccount } from './account'
|
import { createBotAccount } from './account'
|
||||||
import { registerLoaders } from './loaders'
|
import { registerLoaders } from './loaders'
|
||||||
@ -37,6 +37,7 @@ export const start = async (): Promise<void> => {
|
|||||||
ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName })
|
ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName })
|
||||||
|
|
||||||
const db = await getDB()
|
const db = await getDB()
|
||||||
|
const storage = new DbStorage(db)
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
ctx.info('Creating bot account', { attempt: i })
|
ctx.info('Creating bot account', { attempt: i })
|
||||||
try {
|
try {
|
||||||
@ -47,7 +48,7 @@ export const start = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
}
|
}
|
||||||
const aiController = new AIBotController(db, ctx)
|
const aiController = new AIBotController(storage, ctx)
|
||||||
|
|
||||||
const onClose = (): void => {
|
const onClose = (): void => {
|
||||||
void aiController.close()
|
void aiController.close()
|
||||||
|
@ -14,9 +14,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
import { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
||||||
import { MongoClient } from 'mongodb'
|
import { Collection, Db, MongoClient, ObjectId, UpdateFilter, WithId } from 'mongodb'
|
||||||
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
|
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
|
import { HistoryRecord } from './types'
|
||||||
|
|
||||||
const clientRef: MongoClientReference = getMongoClient(config.MongoURL)
|
const clientRef: MongoClientReference = getMongoClient(config.MongoURL)
|
||||||
let client: MongoClient | undefined
|
let client: MongoClient | undefined
|
||||||
@ -34,3 +37,43 @@ export const getDB = (() => {
|
|||||||
export const closeDB: () => Promise<void> = async () => {
|
export const closeDB: () => Promise<void> = async () => {
|
||||||
clientRef.close()
|
clientRef.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DbStorage {
|
||||||
|
private readonly workspacesInfoCollection: Collection<WorkspaceInfoRecord>
|
||||||
|
private readonly historyCollection: Collection<HistoryRecord>
|
||||||
|
|
||||||
|
constructor (private readonly db: Db) {
|
||||||
|
this.workspacesInfoCollection = this.db.collection<WorkspaceInfoRecord>('workspacesInfo')
|
||||||
|
this.historyCollection = this.db.collection<HistoryRecord>('history')
|
||||||
|
}
|
||||||
|
|
||||||
|
async addHistoryRecord (record: HistoryRecord): Promise<ObjectId> {
|
||||||
|
return (await this.historyCollection.insertOne(record)).insertedId
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistoryRecords (workspace: string, objectId: Ref<Doc>): Promise<WithId<HistoryRecord>[]> {
|
||||||
|
return await this.historyCollection
|
||||||
|
.find({ workspace, objectId }, { sort: { timestamp: SortingOrder.Ascending } })
|
||||||
|
.toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeHistoryRecords (_ids: ObjectId[]): Promise<void> {
|
||||||
|
await this.historyCollection.deleteMany({ _id: { $in: _ids } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveWorkspaces (): Promise<WorkspaceInfoRecord[]> {
|
||||||
|
return await this.workspacesInfoCollection.find({ active: true }).toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
async inactiveWorkspace (workspace: string): Promise<void> {
|
||||||
|
await this.workspacesInfoCollection.updateOne({ workspace }, { $set: { active: false } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkspace (workspace: string): Promise<WorkspaceInfoRecord | undefined> {
|
||||||
|
return (await this.workspacesInfoCollection.findOne({ workspace })) ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWorkspace (workspace: string, update: UpdateFilter<WorkspaceInfoRecord>): Promise<void> {
|
||||||
|
await this.workspacesInfoCollection.updateOne({ workspace }, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
29
services/ai-bot/pod-ai-bot/src/types.ts
Normal file
29
services/ai-bot/pod-ai-bot/src/types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// 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 { ObjectId } from 'mongodb'
|
||||||
|
import { Account, Class, Doc, Ref } from '@hcengineering/core'
|
||||||
|
|
||||||
|
export interface HistoryRecord {
|
||||||
|
_id?: ObjectId
|
||||||
|
workspace: string
|
||||||
|
message: string
|
||||||
|
objectId: Ref<Doc>
|
||||||
|
objectClass: Ref<Class<Doc>>
|
||||||
|
role: string
|
||||||
|
user: Ref<Account>
|
||||||
|
tokens: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
129
services/ai-bot/pod-ai-bot/src/utils.ts
Normal file
129
services/ai-bot/pod-ai-bot/src/utils.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
//
|
||||||
|
// 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, { Account, Ref, TxOperations } from '@hcengineering/core'
|
||||||
|
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||||
|
import aiBot from '@hcengineering/ai-bot'
|
||||||
|
import { loginBot } from './account'
|
||||||
|
import chunter, { DirectMessage } from '@hcengineering/chunter'
|
||||||
|
import { deepEqual } from 'fast-equals'
|
||||||
|
import notification from '@hcengineering/notification'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
import { countTokens } from '@hcengineering/openai'
|
||||||
|
import { Tiktoken } from 'tiktoken'
|
||||||
|
|
||||||
|
import { HistoryRecord } from './types'
|
||||||
|
import config from './config'
|
||||||
|
|
||||||
|
export async function login (): Promise<string | undefined> {
|
||||||
|
const token = (await loginBot())?.token
|
||||||
|
|
||||||
|
if (token !== undefined) {
|
||||||
|
return token
|
||||||
|
} else {
|
||||||
|
return (await loginBot())?.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDirect (
|
||||||
|
client: TxOperations,
|
||||||
|
email: string,
|
||||||
|
aiAccount?: PersonAccount
|
||||||
|
): Promise<Ref<DirectMessage> | undefined> {
|
||||||
|
const personAccount = await client.getModel().findOne(contact.class.PersonAccount, { email })
|
||||||
|
|
||||||
|
if (personAccount === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAccounts = await client.findAll(contact.class.PersonAccount, { person: personAccount.person })
|
||||||
|
const accIds: Ref<Account>[] = [aiBot.account.AIBot, ...allAccounts.map(({ _id }) => _id)].sort()
|
||||||
|
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
||||||
|
|
||||||
|
for (const dm of existingDms) {
|
||||||
|
if (deepEqual(dm.members.sort(), accIds)) {
|
||||||
|
return dm._id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmId = await client.createDoc<DirectMessage>(chunter.class.DirectMessage, core.space.Space, {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
private: true,
|
||||||
|
archived: false,
|
||||||
|
members: accIds
|
||||||
|
})
|
||||||
|
|
||||||
|
if (aiAccount === undefined) return dmId
|
||||||
|
const space = await client.findOne(contact.class.PersonSpace, { person: aiAccount.person })
|
||||||
|
if (space === undefined) return dmId
|
||||||
|
await client.createDoc(notification.class.DocNotifyContext, space._id, {
|
||||||
|
user: aiBot.account.AIBot,
|
||||||
|
objectId: dmId,
|
||||||
|
objectClass: chunter.class.DirectMessage,
|
||||||
|
objectSpace: core.space.Space,
|
||||||
|
isPinned: false
|
||||||
|
})
|
||||||
|
|
||||||
|
return dmId
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChatCompletion (
|
||||||
|
client: OpenAI,
|
||||||
|
message: OpenAI.ChatCompletionMessageParam,
|
||||||
|
user?: string,
|
||||||
|
history: OpenAI.ChatCompletionMessageParam[] = []
|
||||||
|
): Promise<OpenAI.ChatCompletion | undefined> {
|
||||||
|
try {
|
||||||
|
return await client.chat.completions.create({
|
||||||
|
messages: [...history, message],
|
||||||
|
model: config.OpenAIModel,
|
||||||
|
user,
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestSummary (
|
||||||
|
aiClient: OpenAI,
|
||||||
|
encoding: Tiktoken,
|
||||||
|
history: HistoryRecord[]
|
||||||
|
): Promise<{
|
||||||
|
summary?: string
|
||||||
|
tokens: number
|
||||||
|
}> {
|
||||||
|
const summaryPrompt: OpenAI.ChatCompletionMessageParam = {
|
||||||
|
content: `Summarize the following messages, keeping the key points: ${history.map((msg) => `${msg.role}: ${msg.message}`).join('\n')}`,
|
||||||
|
role: 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await createChatCompletion(aiClient, summaryPrompt, undefined, [
|
||||||
|
{ role: 'system', content: 'Make a summary of messages history' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const summary = response?.choices[0].message.content
|
||||||
|
|
||||||
|
if (summary == null) {
|
||||||
|
return { tokens: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = response?.usage?.completion_tokens ?? countTokens([{ content: summary, role: 'assistant' }], encoding)
|
||||||
|
|
||||||
|
return { summary, tokens }
|
||||||
|
}
|
@ -25,26 +25,39 @@ import core, {
|
|||||||
TxCreateDoc,
|
TxCreateDoc,
|
||||||
TxOperations,
|
TxOperations,
|
||||||
TxProcessor,
|
TxProcessor,
|
||||||
WorkspaceId,
|
|
||||||
Blob,
|
Blob,
|
||||||
RateLimiter
|
RateLimiter,
|
||||||
|
generateId,
|
||||||
|
TxRemoveDoc,
|
||||||
|
Data
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import aiBot, { AIBotEvent, aiBotAccountEmail, AIBotResponseEvent, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
import aiBot, { AIBotEvent, aiBotAccountEmail, AIBotResponseEvent, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
||||||
import chunter, { Channel, ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
import chunter, { Channel, ChatMessage, DirectMessage, ThreadMessage, TypingInfo } from '@hcengineering/chunter'
|
||||||
import contact, { AvatarType, combineName, getFirstName, getLastName, PersonAccount } from '@hcengineering/contact'
|
import contact, {
|
||||||
import notification from '@hcengineering/notification'
|
AvatarType,
|
||||||
|
combineName,
|
||||||
|
getFirstName,
|
||||||
|
getLastName,
|
||||||
|
Person,
|
||||||
|
PersonAccount
|
||||||
|
} from '@hcengineering/contact'
|
||||||
import { getOrCreateOnboardingChannel } from '@hcengineering/server-analytics-collector-resources'
|
import { getOrCreateOnboardingChannel } from '@hcengineering/server-analytics-collector-resources'
|
||||||
import { deepEqual } from 'fast-equals'
|
|
||||||
import { BlobClient } from '@hcengineering/server-client'
|
import { BlobClient } from '@hcengineering/server-client'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
|
import { countTokens } from '@hcengineering/openai'
|
||||||
|
import { jsonToMarkup, MarkdownParser, markupToText } from '@hcengineering/text'
|
||||||
|
import { WithId } from 'mongodb'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { loginBot } from './account'
|
|
||||||
import { AIBotController } from './controller'
|
import { AIBotController } from './controller'
|
||||||
import { connectPlatform } from './platform'
|
import { connectPlatform } from './platform'
|
||||||
|
import { HistoryRecord } from './types'
|
||||||
|
import { createChatCompletion, getDirect, login, requestSummary } from './utils'
|
||||||
|
|
||||||
const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses
|
const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses
|
||||||
|
const UPDATE_TYPING_TIMEOUT_MS = 1000
|
||||||
|
|
||||||
export class WorkspaceClient {
|
export class WorkspaceClient {
|
||||||
client: Client | undefined
|
client: Client | undefined
|
||||||
@ -56,39 +69,56 @@ export class WorkspaceClient {
|
|||||||
loginDelayMs = 2 * 1000
|
loginDelayMs = 2 * 1000
|
||||||
|
|
||||||
channelByKey = new Map<string, Ref<Channel>>()
|
channelByKey = new Map<string, Ref<Channel>>()
|
||||||
aiAccount: PersonAccount | undefined
|
|
||||||
rate = new RateLimiter(1)
|
rate = new RateLimiter(1)
|
||||||
|
|
||||||
|
aiAccount: PersonAccount | undefined
|
||||||
|
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>()
|
||||||
directByEmail = new Map<string, Ref<DirectMessage>>()
|
directByEmail = new Map<string, Ref<DirectMessage>>()
|
||||||
|
|
||||||
|
historyMap = new Map<Ref<Doc>, WithId<HistoryRecord>[]>()
|
||||||
|
|
||||||
|
summarizing = new Set<Ref<Doc>>()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
readonly transactorUrl: string,
|
readonly transactorUrl: string,
|
||||||
readonly token: string,
|
readonly token: string,
|
||||||
readonly workspace: WorkspaceId,
|
readonly workspace: string,
|
||||||
readonly controller: AIBotController,
|
readonly controller: AIBotController,
|
||||||
readonly ctx: MeasureContext,
|
readonly ctx: MeasureContext,
|
||||||
readonly info: WorkspaceInfoRecord | undefined
|
readonly info: WorkspaceInfoRecord | undefined
|
||||||
) {
|
) {
|
||||||
this.blobClient = new BlobClient(transactorUrl, token, this.workspace)
|
this.blobClient = new BlobClient(transactorUrl, token, { name: this.workspace })
|
||||||
this.opClient = this.initClient()
|
this.opClient = this.initClient()
|
||||||
void this.opClient.then((opClient) => {
|
void this.opClient.then((opClient) => {
|
||||||
this.opClient = opClient
|
this.opClient = opClient
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async login (): Promise<string | undefined> {
|
private async initClient (): Promise<TxOperations> {
|
||||||
this.ctx.info('Logging in: ', this.workspace)
|
await this.tryLogin()
|
||||||
const token = (await loginBot())?.token
|
|
||||||
|
|
||||||
if (token !== undefined) {
|
this.client = await connectPlatform(this.token, this.transactorUrl)
|
||||||
return token
|
const opClient = new TxOperations(this.client, aiBot.account.AIBot)
|
||||||
} else {
|
|
||||||
return (await loginBot())?.token
|
await this.uploadAvatarFile(opClient)
|
||||||
|
const typing = await opClient.findAll(chunter.class.TypingInfo, { user: aiBot.account.AIBot })
|
||||||
|
this.typingMap = new Map(typing.map((t) => [t.objectId, t]))
|
||||||
|
const events = await opClient.findAll(aiBot.class.AIBotEvent, {})
|
||||||
|
void this.processEvents(events)
|
||||||
|
|
||||||
|
this.client.notify = (...txes: Tx[]) => {
|
||||||
|
void this.txHandler(opClient, txes)
|
||||||
}
|
}
|
||||||
|
this.ctx.info('Initialized workspace', { workspace: this.workspace })
|
||||||
|
|
||||||
|
return opClient
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadAvatarFile (client: TxOperations): Promise<void> {
|
private async uploadAvatarFile (client: TxOperations): Promise<void> {
|
||||||
this.ctx.info('Upload avatar file', { workspace: this.workspace.name })
|
this.ctx.info('Upload avatar file', { workspace: this.workspace })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.checkPersonData(client)
|
await this.checkPersonData(client)
|
||||||
@ -101,21 +131,22 @@ export class WorkspaceClient {
|
|||||||
this.info.avatarPath === config.AvatarPath &&
|
this.info.avatarPath === config.AvatarPath &&
|
||||||
this.info.avatarLastModified === lastModified
|
this.info.avatarLastModified === lastModified
|
||||||
) {
|
) {
|
||||||
this.ctx.info('Avatar file already uploaded', { workspace: this.workspace.name, path: config.AvatarPath })
|
this.ctx.info('Avatar file already uploaded', { workspace: this.workspace, path: config.AvatarPath })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(config.AvatarPath)
|
const data = fs.readFileSync(config.AvatarPath)
|
||||||
|
|
||||||
await this.blobClient.upload(this.ctx, config.AvatarName, data.length, config.AvatarContentType, data)
|
await this.blobClient.upload(this.ctx, config.AvatarName, data.length, config.AvatarContentType, data)
|
||||||
await this.controller.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified)
|
await this.controller.updateAvatarInfo(this.workspace, config.AvatarPath, lastModified)
|
||||||
this.ctx.info('Uploaded avatar file', { workspace: this.workspace.name, path: config.AvatarPath })
|
this.ctx.info('Uploaded avatar file', { workspace: this.workspace, path: config.AvatarPath })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.ctx.error('Failed to upload avatar file', { e })
|
this.ctx.error('Failed to upload avatar file', { e })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tryLogin (): Promise<void> {
|
private async tryLogin (): Promise<void> {
|
||||||
const token = await this.login()
|
this.ctx.info('Logging in: ', { workspace: this.workspace })
|
||||||
|
const token = await login()
|
||||||
|
|
||||||
clearTimeout(this.loginTimeout)
|
clearTimeout(this.loginTimeout)
|
||||||
|
|
||||||
@ -136,52 +167,34 @@ export class WorkspaceClient {
|
|||||||
this.ctx.error('Cannot find AI PersonAccount', { email: aiBotAccountEmail })
|
this.ctx.error('Cannot find AI PersonAccount', { email: aiBotAccountEmail })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const person = await client.findOne(contact.class.Person, { _id: this.aiAccount.person })
|
this.aiPerson = await client.findOne(contact.class.Person, { _id: this.aiAccount.person })
|
||||||
|
|
||||||
if (person === undefined) {
|
if (this.aiPerson === undefined) {
|
||||||
this.ctx.error('Cannot find AI Person ', { _id: this.aiAccount.person })
|
this.ctx.error('Cannot find AI Person ', { _id: this.aiAccount.person })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstName = getFirstName(person.name)
|
const firstName = getFirstName(this.aiPerson.name)
|
||||||
const lastName = getLastName(person.name)
|
const lastName = getLastName(this.aiPerson.name)
|
||||||
|
|
||||||
if (lastName !== config.LastName || firstName !== config.FirstName) {
|
if (lastName !== config.LastName || firstName !== config.FirstName) {
|
||||||
await client.update(person, {
|
await client.update(this.aiPerson, {
|
||||||
name: combineName(config.FirstName, config.LastName)
|
name: combineName(config.FirstName, config.LastName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (person.avatar === config.AvatarName) {
|
if (this.aiPerson.avatar === config.AvatarName) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.blobClient.checkFile(this.ctx, config.AvatarName)
|
const exist = await this.blobClient.checkFile(this.ctx, config.AvatarName)
|
||||||
|
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
this.ctx.error('Cannot find file', { file: config.AvatarName, workspace: this.workspace.name })
|
this.ctx.error('Cannot find file', { file: config.AvatarName, workspace: this.workspace })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.diffUpdate(person, { avatar: config.AvatarName as Ref<Blob>, avatarType: AvatarType.IMAGE })
|
await client.diffUpdate(this.aiPerson, { avatar: config.AvatarName as Ref<Blob>, avatarType: AvatarType.IMAGE })
|
||||||
}
|
|
||||||
|
|
||||||
private async initClient (): Promise<TxOperations> {
|
|
||||||
await this.tryLogin()
|
|
||||||
|
|
||||||
this.client = await connectPlatform(this.token, this.transactorUrl)
|
|
||||||
const opClient = new TxOperations(this.client, aiBot.account.AIBot)
|
|
||||||
|
|
||||||
await this.uploadAvatarFile(opClient)
|
|
||||||
const events = await opClient.findAll(aiBot.class.AIBotTransferEvent, {})
|
|
||||||
void this.processEvents(events)
|
|
||||||
|
|
||||||
this.client.notify = (...txes: Tx[]) => {
|
|
||||||
void this.txHandler(opClient, txes)
|
|
||||||
}
|
|
||||||
this.ctx.info('Initialized workspace', this.workspace)
|
|
||||||
|
|
||||||
return opClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getThreadParent (
|
async getThreadParent (
|
||||||
@ -216,8 +229,10 @@ export class WorkspaceClient {
|
|||||||
space: Ref<Space>,
|
space: Ref<Space>,
|
||||||
message: string
|
message: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const op = client.apply(generateId(), 'AIBotTransferEvent')
|
||||||
if (event.messageClass === chunter.class.ChatMessage) {
|
if (event.messageClass === chunter.class.ChatMessage) {
|
||||||
const ref = await client.addCollection<Doc, ChatMessage>(
|
await this.startTyping(client, space, _id, _class)
|
||||||
|
const ref = await op.addCollection<Doc, ChatMessage>(
|
||||||
chunter.class.ChatMessage,
|
chunter.class.ChatMessage,
|
||||||
space,
|
space,
|
||||||
_id,
|
_id,
|
||||||
@ -225,15 +240,17 @@ export class WorkspaceClient {
|
|||||||
event.collection,
|
event.collection,
|
||||||
{ message }
|
{ message }
|
||||||
)
|
)
|
||||||
await client.createMixin(ref, chunter.class.ChatMessage, space, aiBot.mixin.TransferredMessage, {
|
await op.createMixin(ref, chunter.class.ChatMessage, space, aiBot.mixin.TransferredMessage, {
|
||||||
messageId: event.messageId,
|
messageId: event.messageId,
|
||||||
parentMessageId: event.parentMessageId
|
parentMessageId: event.parentMessageId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await this.finishTyping(client, _id)
|
||||||
} else if (event.messageClass === chunter.class.ThreadMessage && event.parentMessageId !== undefined) {
|
} else if (event.messageClass === chunter.class.ThreadMessage && event.parentMessageId !== undefined) {
|
||||||
const parent = await this.getThreadParent(client, event, _id, _class)
|
const parent = await this.getThreadParent(client, event, _id, _class)
|
||||||
|
|
||||||
if (parent !== undefined) {
|
if (parent !== undefined) {
|
||||||
const ref = await client.addCollection<Doc, ThreadMessage>(
|
await this.startTyping(client, space, parent._id, parent._class)
|
||||||
|
const ref = await op.addCollection<Doc, ThreadMessage>(
|
||||||
chunter.class.ThreadMessage,
|
chunter.class.ThreadMessage,
|
||||||
parent.space,
|
parent.space,
|
||||||
parent._id,
|
parent._id,
|
||||||
@ -241,7 +258,7 @@ export class WorkspaceClient {
|
|||||||
event.collection,
|
event.collection,
|
||||||
{ message, objectId: parent.attachedTo, objectClass: parent.attachedToClass }
|
{ message, objectId: parent.attachedTo, objectClass: parent.attachedToClass }
|
||||||
)
|
)
|
||||||
await client.createMixin(
|
await op.createMixin(
|
||||||
ref,
|
ref,
|
||||||
chunter.class.ThreadMessage as Ref<Class<ChatMessage>>,
|
chunter.class.ThreadMessage as Ref<Class<ChatMessage>>,
|
||||||
space,
|
space,
|
||||||
@ -251,40 +268,231 @@ export class WorkspaceClient {
|
|||||||
parentMessageId: event.parentMessageId
|
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[] {
|
||||||
|
const result: OpenAI.ChatCompletionMessageParam[] = []
|
||||||
|
let totalTokens = promptTokens
|
||||||
|
|
||||||
|
for (let i = history.length - 1; i >= 0; i--) {
|
||||||
|
const record = history[i]
|
||||||
|
const tokens = record.tokens
|
||||||
|
|
||||||
|
if (totalTokens + tokens > config.MaxContentTokens) break
|
||||||
|
|
||||||
|
result.unshift({ content: record.message, role: record.role as 'user' | 'assistant' })
|
||||||
|
totalTokens += tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
this.historyMap.set(objectId, historyRecords)
|
||||||
|
return historyRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
async summarizeHistory (
|
||||||
|
toSummarize: WithId<HistoryRecord>[],
|
||||||
|
user: Ref<Account>,
|
||||||
|
objectId: Ref<Doc>,
|
||||||
|
objectClass: Ref<Class<Doc>>
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.summarizing.has(objectId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.summarizing.add(objectId)
|
||||||
|
const { summary, tokens } = await requestSummary(this.controller.aiClient, this.controller.encoding, toSummarize)
|
||||||
|
|
||||||
|
if (summary === undefined) {
|
||||||
|
this.ctx.error('Failed to summarize history', { objectId, objectClass, user })
|
||||||
|
this.summarizing.delete(objectId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryRecord: HistoryRecord = {
|
||||||
|
message: summary,
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: toSummarize[0].timestamp,
|
||||||
|
user,
|
||||||
|
objectId,
|
||||||
|
objectClass,
|
||||||
|
tokens,
|
||||||
|
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)
|
||||||
|
this.historyMap.set(objectId, newHistory)
|
||||||
|
this.summarizing.delete(objectId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushHistory (
|
||||||
|
message: string,
|
||||||
|
role: 'user' | 'assistant',
|
||||||
|
tokens: number,
|
||||||
|
user: Ref<Account>,
|
||||||
|
objectId: Ref<Doc>,
|
||||||
|
objectClass: Ref<Class<Doc>>
|
||||||
|
): Promise<void> {
|
||||||
|
const currentHistory = (await this.getHistory(objectId)) ?? []
|
||||||
|
const newRecord: HistoryRecord = {
|
||||||
|
workspace: this.workspace,
|
||||||
|
message,
|
||||||
|
objectId,
|
||||||
|
objectClass,
|
||||||
|
role,
|
||||||
|
user,
|
||||||
|
tokens,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
const _id = await this.controller.storage.addHistoryRecord(newRecord)
|
||||||
|
currentHistory.push({ ...newRecord, _id })
|
||||||
|
this.historyMap.set(objectId, currentHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
async processResponseEvent (event: AIBotResponseEvent): Promise<void> {
|
async processResponseEvent (event: AIBotResponseEvent): Promise<void> {
|
||||||
const client = await this.opClient
|
const client = await this.opClient
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
if (event.messageClass === chunter.class.ChatMessage) {
|
const op = client.apply(generateId(), 'AIBotResponseEvent')
|
||||||
await client.addCollection<Doc, ChatMessage>(
|
const { user, objectId, objectClass, messageClass } = event
|
||||||
|
const space = hierarchy.isDerived(objectClass, core.class.Space) ? (objectId as Ref<Space>) : event.objectSpace
|
||||||
|
|
||||||
|
await this.startTyping(client, space, objectId, objectClass)
|
||||||
|
|
||||||
|
const promptText = markupToText(event.message)
|
||||||
|
const prompt: OpenAI.ChatCompletionMessageParam = { content: promptText, role: 'user' }
|
||||||
|
|
||||||
|
const promptTokens = countTokens([prompt], this.controller.encoding)
|
||||||
|
const rawHistory = await this.getHistory(objectId)
|
||||||
|
const history = this.toOpenAiHistory(rawHistory, promptTokens)
|
||||||
|
|
||||||
|
if (history.length < rawHistory.length || history.length > config.MaxHistoryRecords) {
|
||||||
|
void this.summarizeHistory(rawHistory, user, objectId, objectClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.pushHistory(promptText, prompt.role, promptTokens, user, objectId, objectClass)
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
const chatCompletion = await createChatCompletion(this.controller.aiClient, prompt, user, history)
|
||||||
|
const end = Date.now()
|
||||||
|
this.ctx.info('Chat completion time: ', { time: end - start })
|
||||||
|
const response = chatCompletion?.choices[0].message.content
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
await this.finishTyping(client, objectId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const responseTokens =
|
||||||
|
chatCompletion?.usage?.completion_tokens ??
|
||||||
|
countTokens([{ content: response, role: 'assistant' }], this.controller.encoding)
|
||||||
|
|
||||||
|
void this.pushHistory(response, 'assistant', responseTokens, user, objectId, objectClass)
|
||||||
|
|
||||||
|
const parser = new MarkdownParser([], '', '')
|
||||||
|
const parseResponse = jsonToMarkup(parser.parse(response))
|
||||||
|
|
||||||
|
if (messageClass === chunter.class.ChatMessage) {
|
||||||
|
await op.addCollection<Doc, ChatMessage>(
|
||||||
chunter.class.ChatMessage,
|
chunter.class.ChatMessage,
|
||||||
event.objectSpace,
|
space,
|
||||||
event.objectId,
|
objectId,
|
||||||
event.objectClass,
|
objectClass,
|
||||||
event.collection,
|
event.collection,
|
||||||
{ message: 'You said: ' + event.message }
|
{ message: parseResponse }
|
||||||
)
|
)
|
||||||
} else if (event.messageClass === chunter.class.ThreadMessage) {
|
} else if (messageClass === chunter.class.ThreadMessage) {
|
||||||
const parent = await client.findOne<ChatMessage>(chunter.class.ChatMessage, {
|
const parent = await client.findOne<ChatMessage>(chunter.class.ChatMessage, {
|
||||||
_id: event.objectId as Ref<ChatMessage>
|
_id: objectId as Ref<ChatMessage>
|
||||||
})
|
})
|
||||||
|
|
||||||
if (parent !== undefined) {
|
if (parent !== undefined) {
|
||||||
await client.addCollection<Doc, ThreadMessage>(
|
await op.addCollection<Doc, ThreadMessage>(
|
||||||
chunter.class.ThreadMessage,
|
chunter.class.ThreadMessage,
|
||||||
event.objectSpace,
|
space,
|
||||||
event.objectId,
|
objectId,
|
||||||
event.objectClass,
|
objectClass,
|
||||||
event.collection,
|
event.collection,
|
||||||
{ message: 'You said: ' + event.message, objectId: parent.attachedTo, objectClass: parent.attachedToClass }
|
{ message: parseResponse, objectId: parent.attachedTo, objectClass: parent.attachedToClass }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.remove(event)
|
await op.remove(event)
|
||||||
|
await this.finishTyping(op, event.objectId)
|
||||||
|
await op.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async processTransferEvent (event: AIBotTransferEvent): Promise<void> {
|
async processTransferEvent (event: AIBotTransferEvent): Promise<void> {
|
||||||
@ -294,53 +502,6 @@ export class WorkspaceClient {
|
|||||||
await client.remove(event)
|
await client.remove(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAccount (email: string): Promise<PersonAccount | undefined> {
|
|
||||||
const client = await this.opClient
|
|
||||||
|
|
||||||
return await client.findOne(contact.class.PersonAccount, { email })
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDirect (email: string): Promise<Ref<DirectMessage> | undefined> {
|
|
||||||
const client = await this.opClient
|
|
||||||
|
|
||||||
const personAccount = await this.getAccount(email)
|
|
||||||
|
|
||||||
if (personAccount === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const allAccounts = await client.findAll(contact.class.PersonAccount, { person: personAccount.person })
|
|
||||||
const accIds: Ref<Account>[] = [aiBot.account.AIBot, ...allAccounts.map(({ _id }) => _id)].sort()
|
|
||||||
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
|
||||||
|
|
||||||
for (const dm of existingDms) {
|
|
||||||
if (deepEqual(dm.members.sort(), accIds)) {
|
|
||||||
return dm._id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmId = await client.createDoc<DirectMessage>(chunter.class.DirectMessage, core.space.Space, {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
private: true,
|
|
||||||
archived: false,
|
|
||||||
members: accIds
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.aiAccount === undefined) return dmId
|
|
||||||
const space = await client.findOne(contact.class.PersonSpace, { person: this.aiAccount.person })
|
|
||||||
if (space === undefined) return dmId
|
|
||||||
await client.createDoc(notification.class.DocNotifyContext, space._id, {
|
|
||||||
user: aiBot.account.AIBot,
|
|
||||||
objectId: dmId,
|
|
||||||
objectClass: chunter.class.DirectMessage,
|
|
||||||
objectSpace: core.space.Space,
|
|
||||||
isPinned: false
|
|
||||||
})
|
|
||||||
|
|
||||||
return dmId
|
|
||||||
}
|
|
||||||
|
|
||||||
async transferToSupport (event: AIBotTransferEvent, channelRef?: Ref<Channel>): Promise<void> {
|
async transferToSupport (event: AIBotTransferEvent, channelRef?: Ref<Channel>): Promise<void> {
|
||||||
const client = await this.opClient
|
const client = await this.opClient
|
||||||
const key = `${event.toEmail}-${event.fromWorkspace}`
|
const key = `${event.toEmail}-${event.fromWorkspace}`
|
||||||
@ -365,14 +526,14 @@ export class WorkspaceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async transferToUserDirect (event: AIBotTransferEvent): Promise<void> {
|
async transferToUserDirect (event: AIBotTransferEvent): Promise<void> {
|
||||||
const direct = this.directByEmail.get(event.toEmail) ?? (await this.getDirect(event.toEmail))
|
const client = await this.opClient
|
||||||
|
const direct = this.directByEmail.get(event.toEmail) ?? (await getDirect(client, event.toEmail, this.aiAccount))
|
||||||
|
|
||||||
if (direct === undefined) {
|
if (direct === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.directByEmail.set(event.toEmail, direct)
|
this.directByEmail.set(event.toEmail, direct)
|
||||||
const client = await this.opClient
|
|
||||||
|
|
||||||
await this.createTransferMessage(client, event, direct, chunter.class.DirectMessage, direct, event.message)
|
await this.createTransferMessage(client, event, direct, chunter.class.DirectMessage, direct, event.message)
|
||||||
}
|
}
|
||||||
@ -440,19 +601,34 @@ export class WorkspaceClient {
|
|||||||
await this.opClient.close()
|
await this.opClient.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ctx.info('Closed workspace client: ', this.workspace)
|
this.ctx.info('Closed workspace client: ', { workspace: this.workspace })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async txHandler (client: TxOperations, txes: Tx[]): Promise<void> {
|
private async handleCreateTx (tx: TxCreateDoc<Doc>): Promise<void> {
|
||||||
const hierarchy = client.getHierarchy()
|
if (tx.objectClass === aiBot.class.AIBotResponseEvent) {
|
||||||
|
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AIBotResponseEvent>)
|
||||||
|
await this.processResponseEvent(doc)
|
||||||
|
} else if (tx.objectClass === aiBot.class.AIBotTransferEvent) {
|
||||||
|
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AIBotTransferEvent>)
|
||||||
|
await this.processTransferEvent(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resultTxes = txes
|
private async handleRemoveTx (tx: TxRemoveDoc<Doc>): Promise<void> {
|
||||||
.map((a) => TxProcessor.extractTx(a) as TxCreateDoc<AIBotEvent>)
|
if (tx.objectClass === chunter.class.TypingInfo && this.typingMap.has(tx.objectId)) {
|
||||||
.filter(
|
this.typingMap.delete(tx.objectId)
|
||||||
(tx) => tx._class === core.class.TxCreateDoc && hierarchy.isDerived(tx.objectClass, aiBot.class.AIBotEvent)
|
}
|
||||||
)
|
}
|
||||||
.map((tx) => TxProcessor.createDoc2Doc(tx))
|
|
||||||
|
|
||||||
await this.processEvents(resultTxes)
|
private async txHandler (_: TxOperations, txes: Tx[]): Promise<void> {
|
||||||
|
for (const ttx of txes) {
|
||||||
|
const tx = TxProcessor.extractTx(ttx)
|
||||||
|
|
||||||
|
if (tx._class === core.class.TxCreateDoc) {
|
||||||
|
await this.handleCreateTx(tx as TxCreateDoc<Doc>)
|
||||||
|
} else if (tx._class === core.class.TxRemoveDoc) {
|
||||||
|
await this.handleRemoveTx(tx as TxRemoveDoc<Doc>)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user