mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-29 11:43:49 +00:00
Added summarization action for meeting minutes (#8143)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
* Added summarization action for meeting minutes Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> * tweaks Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> * fmt Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> * ff Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> --------- Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
e8a91c2c03
commit
9d6048f06f
@ -118,6 +118,21 @@ function defineMessageActions (builder: Builder): void {
|
|||||||
chunter.action.DeleteChatMessage
|
chunter.action.DeleteChatMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: chunter.actionImpl.SummarizeMessages,
|
||||||
|
label: chunter.string.SummarizeMessages,
|
||||||
|
icon: view.icon.Feather,
|
||||||
|
input: 'focus',
|
||||||
|
category: chunter.category.Chunter,
|
||||||
|
target: core.class.Doc,
|
||||||
|
context: { mode: ['context', 'browser'], group: 'tools' },
|
||||||
|
visibilityTester: chunter.function.CanSummarizeMessages
|
||||||
|
},
|
||||||
|
chunter.action.SummarizeMessages
|
||||||
|
)
|
||||||
|
|
||||||
createAction(
|
createAction(
|
||||||
builder,
|
builder,
|
||||||
{
|
{
|
||||||
|
@ -56,7 +56,8 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
OpenInSidebar: '' as ViewAction,
|
OpenInSidebar: '' as ViewAction,
|
||||||
TranslateMessage: '' as ViewAction,
|
TranslateMessage: '' as ViewAction,
|
||||||
ShowOriginalMessage: '' as ViewAction,
|
ShowOriginalMessage: '' as ViewAction,
|
||||||
StartConversation: '' as ViewAction
|
StartConversation: '' as ViewAction,
|
||||||
|
SummarizeMessages: '' as ViewAction
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
Chunter: '' as Ref<ActionCategory>
|
Chunter: '' as Ref<ActionCategory>
|
||||||
|
@ -48,6 +48,9 @@
|
|||||||
|
|
||||||
const checkEmoji = (nodes: MarkupNode[]): boolean => {
|
const checkEmoji = (nodes: MarkupNode[]): boolean => {
|
||||||
const matches: boolean[] = []
|
const matches: boolean[] = []
|
||||||
|
if (nodes.some((node) => node.type !== 'text')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
const reg = node.text?.match(/\P{Emoji}/gu)
|
const reg = node.text?.match(/\P{Emoji}/gu)
|
||||||
matches.push(reg != null && reg.length > 0 && [65039, 65038, 8205].every((code) => code !== reg[0].charCodeAt(0)))
|
matches.push(reg != null && reg.length > 0 && [65039, 65038, 8205].every((code) => code !== reg[0].charCodeAt(0)))
|
||||||
|
@ -12,16 +12,18 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { concatLink, type Markup, type Ref } from '@hcengineering/core'
|
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
|
||||||
import presentation from '@hcengineering/presentation'
|
|
||||||
import {
|
import {
|
||||||
type ConnectMeetingRequest,
|
type ConnectMeetingRequest,
|
||||||
type DisconnectMeetingRequest,
|
type DisconnectMeetingRequest,
|
||||||
|
type SummarizeMessagesRequest,
|
||||||
|
type SummarizeMessagesResponse,
|
||||||
type TranslateRequest,
|
type TranslateRequest,
|
||||||
type TranslateResponse
|
type TranslateResponse
|
||||||
} from '@hcengineering/ai-bot'
|
} from '@hcengineering/ai-bot'
|
||||||
|
import { type Class, concatLink, type Doc, type Markup, type Ref } from '@hcengineering/core'
|
||||||
import { type Room, type RoomLanguage } from '@hcengineering/love'
|
import { type Room, type RoomLanguage } from '@hcengineering/love'
|
||||||
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
|
import presentation from '@hcengineering/presentation'
|
||||||
|
|
||||||
import aiBot from './plugin'
|
import aiBot from './plugin'
|
||||||
|
|
||||||
@ -54,6 +56,43 @@ export async function translate (text: Markup, lang: string): Promise<TranslateR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function summarizeMessages (
|
||||||
|
lang: string,
|
||||||
|
target: Ref<Doc>,
|
||||||
|
targetClass: Ref<Class<Doc>>
|
||||||
|
): Promise<SummarizeMessagesResponse | undefined> {
|
||||||
|
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
|
||||||
|
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||||
|
|
||||||
|
if (url === '' || token === '') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const req: SummarizeMessagesRequest = {
|
||||||
|
target,
|
||||||
|
targetClass,
|
||||||
|
lang
|
||||||
|
}
|
||||||
|
const resp = await fetch(concatLink(url, '/summarize'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(req)
|
||||||
|
})
|
||||||
|
if (!resp.ok) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await resp.json()) as SummarizeMessagesResponse
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function connectMeeting (
|
export async function connectMeeting (
|
||||||
roomId: Ref<Room>,
|
roomId: Ref<Room>,
|
||||||
language: RoomLanguage,
|
language: RoomLanguage,
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import { Class, Doc, Markup, PersonId, Ref, Space, Timestamp } from '@hcengineering/core'
|
import { Class, Doc, Markup, PersonId, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||||
import { Room, RoomLanguage } from '@hcengineering/love'
|
import { Room, RoomLanguage } from '@hcengineering/love'
|
||||||
import { Person } from '@hcengineering/contact'
|
import { Contact, Person } from '@hcengineering/contact'
|
||||||
import { ChatMessage } from '@hcengineering/chunter'
|
import { ChatMessage } from '@hcengineering/chunter'
|
||||||
|
|
||||||
export interface AIEventRequest {
|
export interface AIEventRequest {
|
||||||
@ -35,6 +35,26 @@ export interface TranslateRequest {
|
|||||||
lang: string
|
lang: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PersonMessage {
|
||||||
|
personRef: Ref<Contact>
|
||||||
|
personName: string
|
||||||
|
|
||||||
|
time: Timestamp
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummarizeMessagesRequest {
|
||||||
|
lang: string
|
||||||
|
|
||||||
|
target: Ref<Doc>
|
||||||
|
targetClass: Ref<Class<Doc>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummarizeMessagesResponse {
|
||||||
|
text: Markup
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface TranslateResponse {
|
export interface TranslateResponse {
|
||||||
text: Markup
|
text: Markup
|
||||||
lang: string
|
lang: string
|
||||||
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "Prohlížíte vlákno z archivovaného kanálu",
|
"ViewingThreadFromArchivedChannel": "Prohlížíte vlákno z archivovaného kanálu",
|
||||||
"ViewingArchivedChannel": "Prohlížíte archivovaný kanál",
|
"ViewingArchivedChannel": "Prohlížíte archivovaný kanál",
|
||||||
"OpenChatInSidebar": "Otevřít chat v postranním panelu",
|
"OpenChatInSidebar": "Otevřít chat v postranním panelu",
|
||||||
"NoThreadsYet": "Zatím nejsou žádné vlákna."
|
"NoThreadsYet": "Zatím nejsou žádné vlákna.",
|
||||||
|
"SummarizeMessages": "Shrnout diskuzi"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -132,6 +132,7 @@
|
|||||||
"ViewingArchivedChannel": "Sie sehen einen archivierten Kanal",
|
"ViewingArchivedChannel": "Sie sehen einen archivierten Kanal",
|
||||||
"OpenChatInSidebar": "Chat in Seitenleiste öffnen",
|
"OpenChatInSidebar": "Chat in Seitenleiste öffnen",
|
||||||
"ResolveThread": "Thread abschließen",
|
"ResolveThread": "Thread abschließen",
|
||||||
"NoThreadsYet": "Es gibt noch keine Threads."
|
"NoThreadsYet": "Es gibt noch keine Threads.",
|
||||||
|
"SummarizeMessages": "Diskussion zusammenfassen"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -132,6 +132,7 @@
|
|||||||
"ViewingArchivedChannel": "You are viewing an archived channel",
|
"ViewingArchivedChannel": "You are viewing an archived channel",
|
||||||
"OpenChatInSidebar": "Open chat in sidebar",
|
"OpenChatInSidebar": "Open chat in sidebar",
|
||||||
"ResolveThread": "Resolve",
|
"ResolveThread": "Resolve",
|
||||||
"NoThreadsYet": "There are no threads yet."
|
"NoThreadsYet": "There are no threads yet.",
|
||||||
|
"SummarizeMessages": "Summarize discussion"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "Estás viendo un hilo de un canal archivado",
|
"ViewingThreadFromArchivedChannel": "Estás viendo un hilo de un canal archivado",
|
||||||
"ViewingArchivedChannel": "Estás viendo un canal archivado",
|
"ViewingArchivedChannel": "Estás viendo un canal archivado",
|
||||||
"OpenChatInSidebar": "Abrir chat en la barra lateral",
|
"OpenChatInSidebar": "Abrir chat en la barra lateral",
|
||||||
"NoThreadsYet": "No hay hilos todavía."
|
"NoThreadsYet": "No hay hilos todavía.",
|
||||||
|
"SummarizeMessages": "Resumir la discusión"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "Vous consultez un fil de discussion d'un canal archivé",
|
"ViewingThreadFromArchivedChannel": "Vous consultez un fil de discussion d'un canal archivé",
|
||||||
"ViewingArchivedChannel": "Vous consultez un canal archivé",
|
"ViewingArchivedChannel": "Vous consultez un canal archivé",
|
||||||
"OpenChatInSidebar": "Ouvrir le chat dans la barre latérale",
|
"OpenChatInSidebar": "Ouvrir le chat dans la barre latérale",
|
||||||
"NoThreadsYet": "Il n'y a pas encore de fils de discussion."
|
"NoThreadsYet": "Il n'y a pas encore de fils de discussion.",
|
||||||
|
"SummarizeMessages": "Résumer la discussion"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "Stai visualizzando una discussione da un canale archiviato",
|
"ViewingThreadFromArchivedChannel": "Stai visualizzando una discussione da un canale archiviato",
|
||||||
"ViewingArchivedChannel": "Stai visualizzando un canale archiviato",
|
"ViewingArchivedChannel": "Stai visualizzando un canale archiviato",
|
||||||
"OpenChatInSidebar": "Apri chat nella barra laterale",
|
"OpenChatInSidebar": "Apri chat nella barra laterale",
|
||||||
"NoThreadsYet": "Non ci sono ancora discussioni."
|
"NoThreadsYet": "Non ci sono ancora discussioni.",
|
||||||
|
"SummarizeMessages": "Riassumere la discussione"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "Está a visualizar uma conversa em cadeia de um canal arquivado",
|
"ViewingThreadFromArchivedChannel": "Está a visualizar uma conversa em cadeia de um canal arquivado",
|
||||||
"ViewingArchivedChannel": "Está a visualizar um canal arquivado",
|
"ViewingArchivedChannel": "Está a visualizar um canal arquivado",
|
||||||
"OpenChatInSidebar": "Abrir chat na barra lateral",
|
"OpenChatInSidebar": "Abrir chat na barra lateral",
|
||||||
"NoThreadsYet": "Não há conversas ainda."
|
"NoThreadsYet": "Não há conversas ainda.",
|
||||||
|
"SummarizeMessages": "Resumir discussão"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -132,6 +132,7 @@
|
|||||||
"ViewingArchivedChannel": "Вы просматриваете архивированный канал",
|
"ViewingArchivedChannel": "Вы просматриваете архивированный канал",
|
||||||
"OpenChatInSidebar": "Открыть чат в боковой панели",
|
"OpenChatInSidebar": "Открыть чат в боковой панели",
|
||||||
"ResolveThread": "Пометить завершенным",
|
"ResolveThread": "Пометить завершенным",
|
||||||
"NoThreadsYet": "Пока нет обсуждений."
|
"NoThreadsYet": "Пока нет обсуждений.",
|
||||||
|
"SummarizeMessages": "Подвести итоги обсуждения"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -131,6 +131,7 @@
|
|||||||
"ViewingThreadFromArchivedChannel": "你正在查看已归档频道的线程",
|
"ViewingThreadFromArchivedChannel": "你正在查看已归档频道的线程",
|
||||||
"ViewingArchivedChannel": "你正在查看已归档频道",
|
"ViewingArchivedChannel": "你正在查看已归档频道",
|
||||||
"OpenChatInSidebar": "在侧边栏中打开聊天",
|
"OpenChatInSidebar": "在侧边栏中打开聊天",
|
||||||
"NoThreadsYet": "还没有线程。"
|
"NoThreadsYet": "还没有线程。",
|
||||||
|
"SummarizeMessages": "总结讨论"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"fast-equals": "^5.2.2",
|
"fast-equals": "^5.2.2",
|
||||||
"svelte": "^4.2.19",
|
"svelte": "^4.2.19",
|
||||||
"@hcengineering/text-editor-resources": "^0.6.0",
|
"@hcengineering/text-editor-resources": "^0.6.0",
|
||||||
"@hcengineering/text-editor": "^0.6.0"
|
"@hcengineering/text-editor": "^0.6.0",
|
||||||
|
"@hcengineering/love": "^0.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,9 @@ import {
|
|||||||
translateMessage,
|
translateMessage,
|
||||||
showOriginalMessage,
|
showOriginalMessage,
|
||||||
canTranslateMessage,
|
canTranslateMessage,
|
||||||
startConversationAction
|
startConversationAction,
|
||||||
|
summarizeMessages,
|
||||||
|
canSummarizeMessages
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
export { default as ChannelEmbeddedContent } from './components/ChannelEmbeddedContent.svelte'
|
export { default as ChannelEmbeddedContent } from './components/ChannelEmbeddedContent.svelte'
|
||||||
@ -213,6 +215,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
CloseChatWidgetTab: closeChatWidgetTab,
|
CloseChatWidgetTab: closeChatWidgetTab,
|
||||||
OpenChannelInSidebar: openChannelInSidebar,
|
OpenChannelInSidebar: openChannelInSidebar,
|
||||||
CanTranslateMessage: canTranslateMessage,
|
CanTranslateMessage: canTranslateMessage,
|
||||||
|
CanSummarizeMessages: canSummarizeMessages,
|
||||||
OpenThreadInSidebar: openThreadInSidebar,
|
OpenThreadInSidebar: openThreadInSidebar,
|
||||||
LocationDataResolver: locationDataResolver
|
LocationDataResolver: locationDataResolver
|
||||||
},
|
},
|
||||||
@ -226,6 +229,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
ReplyToThread: replyToThread,
|
ReplyToThread: replyToThread,
|
||||||
OpenInSidebar: openChannelInSidebarAction,
|
OpenInSidebar: openChannelInSidebarAction,
|
||||||
TranslateMessage: translateMessage,
|
TranslateMessage: translateMessage,
|
||||||
|
SummarizeMessages: summarizeMessages,
|
||||||
ShowOriginalMessage: showOriginalMessage,
|
ShowOriginalMessage: showOriginalMessage,
|
||||||
StartConversation: startConversationAction
|
StartConversation: startConversationAction
|
||||||
}
|
}
|
||||||
|
@ -20,24 +20,26 @@ import activity, {
|
|||||||
type DocUpdateMessage
|
type DocUpdateMessage
|
||||||
} from '@hcengineering/activity'
|
} from '@hcengineering/activity'
|
||||||
import { isReactionMessage } from '@hcengineering/activity-resources'
|
import { isReactionMessage } from '@hcengineering/activity-resources'
|
||||||
|
import aiBot from '@hcengineering/ai-bot'
|
||||||
|
import { summarizeMessages as aiSummarizeMessages, translate as aiTranslate } from '@hcengineering/ai-bot-resources'
|
||||||
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
|
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
|
||||||
import contact, { getName, getCurrentEmployee, type Employee, type Person } from '@hcengineering/contact'
|
import contact, { getCurrentEmployee, getName, type Employee, type Person } from '@hcengineering/contact'
|
||||||
import {
|
import {
|
||||||
PersonIcon,
|
|
||||||
employeeByAccountStore,
|
employeeByAccountStore,
|
||||||
employeeByIdStore,
|
employeeByIdStore,
|
||||||
|
PersonIcon,
|
||||||
personRefByAccountUuidStore
|
personRefByAccountUuidStore
|
||||||
} from '@hcengineering/contact-resources'
|
} from '@hcengineering/contact-resources'
|
||||||
import core, {
|
import core, {
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
|
notEmpty,
|
||||||
|
type AccountUuid,
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
type Client,
|
||||||
type Doc,
|
type Doc,
|
||||||
type Ref,
|
type Ref,
|
||||||
type Space,
|
type Space,
|
||||||
type Timestamp,
|
type Timestamp
|
||||||
type AccountUuid,
|
|
||||||
notEmpty
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||||
import {
|
import {
|
||||||
@ -45,19 +47,18 @@ import {
|
|||||||
isActivityNotification,
|
isActivityNotification,
|
||||||
isMentionNotification
|
isMentionNotification
|
||||||
} from '@hcengineering/notification-resources'
|
} from '@hcengineering/notification-resources'
|
||||||
import { translate, type Asset, getMetadata } from '@hcengineering/platform'
|
import { getMetadata, translate, type Asset } from '@hcengineering/platform'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { type AnySvelteComponent, languageStore } from '@hcengineering/ui'
|
import { languageStore, type AnySvelteComponent } from '@hcengineering/ui'
|
||||||
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
|
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
|
||||||
import { get, writable, type Unsubscriber } from 'svelte/store'
|
import { get, writable, type Unsubscriber } from 'svelte/store'
|
||||||
import aiBot from '@hcengineering/ai-bot'
|
|
||||||
import { translate as aiTranslate } from '@hcengineering/ai-bot-resources'
|
|
||||||
|
|
||||||
import ChannelIcon from './components/ChannelIcon.svelte'
|
import ChannelIcon from './components/ChannelIcon.svelte'
|
||||||
import DirectIcon from './components/DirectIcon.svelte'
|
import DirectIcon from './components/DirectIcon.svelte'
|
||||||
import { openChannelInSidebar, resetChunterLocIfEqual } from './navigation'
|
import { openChannelInSidebar, resetChunterLocIfEqual } from './navigation'
|
||||||
import chunter from './plugin'
|
import chunter from './plugin'
|
||||||
import { shownTranslatedMessagesStore, translatedMessagesStore, translatingMessagesStore } from './stores'
|
import { shownTranslatedMessagesStore, translatedMessagesStore, translatingMessagesStore } from './stores'
|
||||||
|
import love, { type MeetingMinutes } from '@hcengineering/love'
|
||||||
|
|
||||||
export async function getDmName (client: Client, space?: Space): Promise<string> {
|
export async function getDmName (client: Client, space?: Space): Promise<string> {
|
||||||
if (space === undefined) {
|
if (space === undefined) {
|
||||||
@ -531,6 +532,24 @@ export async function canTranslateMessage (): Promise<boolean> {
|
|||||||
return url !== ''
|
return url !== ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function summarizeMessages (doc: Doc): Promise<void> {
|
||||||
|
await aiSummarizeMessages(get(languageStore), doc._id, doc._class)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function canSummarizeMessages (doc: Doc): Promise<boolean> {
|
||||||
|
if (doc?._id === undefined) return false
|
||||||
|
|
||||||
|
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
|
||||||
|
if (url === '') return false
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
if (!hierarchy.isDerived(doc._class, love.class.MeetingMinutes)) return false
|
||||||
|
|
||||||
|
return ((doc as MeetingMinutes).transcription ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
export async function startConversationAction (docs?: Employee | Employee[]): Promise<void> {
|
export async function startConversationAction (docs?: Employee | Employee[]): Promise<void> {
|
||||||
if (docs === undefined) return
|
if (docs === undefined) return
|
||||||
const employees = Array.isArray(docs) ? docs : [docs]
|
const employees = Array.isArray(docs) ? docs : [docs]
|
||||||
|
@ -203,7 +203,8 @@ export default plugin(chunterId, {
|
|||||||
StartConversation: '' as IntlString,
|
StartConversation: '' as IntlString,
|
||||||
ViewingThreadFromArchivedChannel: '' as IntlString,
|
ViewingThreadFromArchivedChannel: '' as IntlString,
|
||||||
ViewingArchivedChannel: '' as IntlString,
|
ViewingArchivedChannel: '' as IntlString,
|
||||||
OpenChatInSidebar: '' as IntlString
|
OpenChatInSidebar: '' as IntlString,
|
||||||
|
SummarizeMessages: '' as IntlString
|
||||||
},
|
},
|
||||||
ids: {
|
ids: {
|
||||||
DMNotification: '' as Ref<NotificationType>,
|
DMNotification: '' as Ref<NotificationType>,
|
||||||
@ -221,11 +222,13 @@ export default plugin(chunterId, {
|
|||||||
LeaveChannel: '' as Ref<Action>,
|
LeaveChannel: '' as Ref<Action>,
|
||||||
RemoveChannel: '' as Ref<Action>,
|
RemoveChannel: '' as Ref<Action>,
|
||||||
TranslateMessage: '' as Ref<Action>,
|
TranslateMessage: '' as Ref<Action>,
|
||||||
|
SummarizeMessages: '' as Ref<Action>,
|
||||||
ShowOriginalMessage: '' as Ref<Action>,
|
ShowOriginalMessage: '' as Ref<Action>,
|
||||||
CloseConversation: '' as Ref<Action>
|
CloseConversation: '' as Ref<Action>
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
|
CanSummarizeMessages: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
OpenThreadInSidebar: '' as Resource<
|
OpenThreadInSidebar: '' as Resource<
|
||||||
(
|
(
|
||||||
_id: Ref<ActivityMessage>,
|
_id: Ref<ActivityMessage>,
|
||||||
|
@ -31,6 +31,7 @@ interface Config {
|
|||||||
OpenAIModel: OpenAI.ChatModel
|
OpenAIModel: OpenAI.ChatModel
|
||||||
OpenAIBaseUrl: string
|
OpenAIBaseUrl: string
|
||||||
OpenAITranslateModel: OpenAI.ChatModel
|
OpenAITranslateModel: OpenAI.ChatModel
|
||||||
|
OpenAISummaryModel: OpenAI.ChatModel
|
||||||
MaxContentTokens: number
|
MaxContentTokens: number
|
||||||
MaxHistoryRecords: number
|
MaxHistoryRecords: number
|
||||||
Port: number
|
Port: number
|
||||||
@ -56,6 +57,7 @@ const config: Config = (() => {
|
|||||||
OpenAIKey: process.env.OPENAI_API_KEY ?? '',
|
OpenAIKey: process.env.OPENAI_API_KEY ?? '',
|
||||||
OpenAIModel: (process.env.OPENAI_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
OpenAIModel: (process.env.OPENAI_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
||||||
OpenAITranslateModel: (process.env.OPENAI_TRANSLATE_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
OpenAITranslateModel: (process.env.OPENAI_TRANSLATE_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
||||||
|
OpenAISummaryModel: (process.env.OPENAI_SUMMARY_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
|
||||||
OpenAIBaseUrl: process.env.OPENAI_BASE_URL ?? '',
|
OpenAIBaseUrl: process.env.OPENAI_BASE_URL ?? '',
|
||||||
MaxContentTokens: parseNumber(process.env.MAX_CONTENT_TOKENS) ?? 128 * 100,
|
MaxContentTokens: parseNumber(process.env.MAX_CONTENT_TOKENS) ?? 128 * 100,
|
||||||
MaxHistoryRecords: parseNumber(process.env.MAX_HISTORY_RECORDS) ?? 500,
|
MaxHistoryRecords: parseNumber(process.env.MAX_HISTORY_RECORDS) ?? 500,
|
||||||
|
@ -13,32 +13,48 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { isWorkspaceLoginInfo } from '@hcengineering/account-client'
|
||||||
import {
|
import {
|
||||||
AIEventRequest,
|
AIEventRequest,
|
||||||
ConnectMeetingRequest,
|
ConnectMeetingRequest,
|
||||||
DisconnectMeetingRequest,
|
DisconnectMeetingRequest,
|
||||||
IdentityResponse,
|
IdentityResponse,
|
||||||
|
PersonMessage,
|
||||||
PostTranscriptRequest,
|
PostTranscriptRequest,
|
||||||
|
SummarizeMessagesRequest,
|
||||||
|
SummarizeMessagesResponse,
|
||||||
TranslateRequest,
|
TranslateRequest,
|
||||||
TranslateResponse
|
TranslateResponse
|
||||||
} from '@hcengineering/ai-bot'
|
} from '@hcengineering/ai-bot'
|
||||||
import { AccountUuid, MeasureContext, Ref, SocialId, type WorkspaceIds, type WorkspaceUuid } from '@hcengineering/core'
|
import core, {
|
||||||
|
AccountUuid,
|
||||||
|
MeasureContext,
|
||||||
|
PersonId,
|
||||||
|
Ref,
|
||||||
|
SocialId,
|
||||||
|
SortingOrder,
|
||||||
|
toIdMap,
|
||||||
|
type WorkspaceIds,
|
||||||
|
type WorkspaceUuid
|
||||||
|
} from '@hcengineering/core'
|
||||||
import { Room } from '@hcengineering/love'
|
import { Room } from '@hcengineering/love'
|
||||||
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
|
||||||
import { getAccountClient } from '@hcengineering/server-client'
|
import { getAccountClient } from '@hcengineering/server-client'
|
||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
import { htmlToMarkup, markupToJSON, jsonToHTML } from '@hcengineering/text'
|
import { htmlToMarkup, jsonToHTML, jsonToMarkup, markupToJSON } from '@hcengineering/text'
|
||||||
import { isWorkspaceLoginInfo } from '@hcengineering/account-client'
|
|
||||||
import { encodingForModel } from 'js-tiktoken'
|
import { encodingForModel } from 'js-tiktoken'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
import chunter from '@hcengineering/chunter'
|
||||||
import { StorageAdapter } from '@hcengineering/server-core'
|
import { StorageAdapter } from '@hcengineering/server-core'
|
||||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||||
|
import { markdownToMarkup, markupToMarkdown } from '@hcengineering/text-markdown'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { DbStorage } from './storage'
|
import { DbStorage } from './storage'
|
||||||
import { WorkspaceClient } from './workspace/workspaceClient'
|
|
||||||
import { translateHtml } from './utils/openai'
|
|
||||||
import { tryAssignToWorkspace } from './utils/account'
|
import { tryAssignToWorkspace } from './utils/account'
|
||||||
|
import { summarizeMessages, translateHtml } from './utils/openai'
|
||||||
|
import { WorkspaceClient } from './workspace/workspaceClient'
|
||||||
|
import contact, { Contact, getName } from '@hcengineering/contact'
|
||||||
|
|
||||||
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
||||||
|
|
||||||
@ -204,6 +220,109 @@ export class AIControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async summarizeMessages (
|
||||||
|
workspace: WorkspaceUuid,
|
||||||
|
req: SummarizeMessagesRequest
|
||||||
|
): Promise<SummarizeMessagesResponse | undefined> {
|
||||||
|
if (this.openai === undefined) return
|
||||||
|
if (req.target === undefined || req.targetClass === undefined) return
|
||||||
|
|
||||||
|
const wsClient = await this.getWorkspaceClient(workspace)
|
||||||
|
if (wsClient === undefined) return
|
||||||
|
|
||||||
|
const opClient = await wsClient.opClient
|
||||||
|
if (opClient === undefined) return
|
||||||
|
|
||||||
|
const client = wsClient.client
|
||||||
|
if (client === undefined) return
|
||||||
|
|
||||||
|
const target = await client.findOne(req.targetClass, { _id: req.target })
|
||||||
|
if (target === undefined) return
|
||||||
|
|
||||||
|
const messages = await client.findAll(
|
||||||
|
chunter.class.ChatMessage,
|
||||||
|
{
|
||||||
|
attachedTo: target._id,
|
||||||
|
collection: { $in: ['messages', 'transcription'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: { createdOn: SortingOrder.Ascending },
|
||||||
|
limit: 5000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const personIds = new Set<PersonId>()
|
||||||
|
for (const m of messages) {
|
||||||
|
if (m.createdBy !== undefined) personIds.add(m.createdBy)
|
||||||
|
}
|
||||||
|
const identities = await client.findAll(contact.class.SocialIdentity, { key: { $in: Array.from(personIds) } })
|
||||||
|
const contacts = await client.findAll(contact.class.Contact, { _id: { $in: identities.map((i) => i.attachedTo) } })
|
||||||
|
const contactById = toIdMap(contacts)
|
||||||
|
const contactByPersonId = new Map<PersonId, Contact>()
|
||||||
|
for (const identity of identities) {
|
||||||
|
const contact = contactById.get(identity.attachedTo)
|
||||||
|
if (contact !== undefined) contactByPersonId.set(identity.key, contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesToSummarize: PersonMessage[] = []
|
||||||
|
|
||||||
|
for (const m of messages) {
|
||||||
|
const author = m.createdBy
|
||||||
|
if (author === undefined) continue
|
||||||
|
|
||||||
|
const contact = contactByPersonId.get(author)
|
||||||
|
if (contact === undefined) continue
|
||||||
|
|
||||||
|
const personName = getName(client.getHierarchy(), contact)
|
||||||
|
const text = markupToMarkdown(markupToJSON(m.message))
|
||||||
|
|
||||||
|
const lastPiece = messagesToSummarize[messagesToSummarize.length - 1]
|
||||||
|
if (lastPiece?.personRef === contact._id) {
|
||||||
|
lastPiece.text += (m.collection === 'transcription' ? ' ' : '\n') + text
|
||||||
|
} else {
|
||||||
|
messagesToSummarize.push({
|
||||||
|
personRef: contact._id,
|
||||||
|
personName,
|
||||||
|
time: m.createdOn ?? 0,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = await summarizeMessages(this.openai, messagesToSummarize, req.lang)
|
||||||
|
if (summary === undefined) return
|
||||||
|
|
||||||
|
const summaryMarkup = jsonToMarkup(markdownToMarkup(summary))
|
||||||
|
|
||||||
|
const lastMessage = await client.findOne(
|
||||||
|
chunter.class.ChatMessage,
|
||||||
|
{
|
||||||
|
attachedTo: target._id,
|
||||||
|
collection: { $in: ['messages', 'transcription', 'summary'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: { createdOn: SortingOrder.Descending },
|
||||||
|
limit: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const op = opClient.apply(undefined, 'AISummarizeMessagesRequestEvent')
|
||||||
|
|
||||||
|
if (lastMessage?.collection === 'summary' && lastMessage.createdBy === opClient.user) {
|
||||||
|
await op.update(lastMessage, { message: summaryMarkup, editedOn: Date.now() })
|
||||||
|
} else {
|
||||||
|
await op.addCollection(chunter.class.ChatMessage, core.space.Workspace, target._id, target._class, 'summary', {
|
||||||
|
message: summaryMarkup
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await op.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: summaryMarkup,
|
||||||
|
lang: req.lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async processEvent (workspace: WorkspaceUuid, events: AIEventRequest[]): Promise<void> {
|
async processEvent (workspace: WorkspaceUuid, events: AIEventRequest[]): Promise<void> {
|
||||||
if (this.openai === undefined) return
|
if (this.openai === undefined) return
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@ import {
|
|||||||
ConnectMeetingRequest,
|
ConnectMeetingRequest,
|
||||||
DisconnectMeetingRequest,
|
DisconnectMeetingRequest,
|
||||||
AIEventRequest,
|
AIEventRequest,
|
||||||
PostTranscriptRequest
|
PostTranscriptRequest,
|
||||||
|
SummarizeMessagesRequest
|
||||||
} from '@hcengineering/ai-bot'
|
} from '@hcengineering/ai-bot'
|
||||||
import { extractToken } from '@hcengineering/server-client'
|
import { extractToken } from '@hcengineering/server-client'
|
||||||
import { MeasureContext } from '@hcengineering/core'
|
import { MeasureContext } from '@hcengineering/core'
|
||||||
@ -73,6 +74,23 @@ export function createServer (controller: AIControl, ctx: MeasureContext): Expre
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/summarize',
|
||||||
|
wrapRequest(async (req, res, token) => {
|
||||||
|
if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') {
|
||||||
|
throw new ApiError(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await controller.summarizeMessages(token.workspace, req.body as SummarizeMessagesRequest)
|
||||||
|
if (response === undefined) {
|
||||||
|
throw new ApiError(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
res.json(response)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
'/connect',
|
'/connect',
|
||||||
wrapRequest(async (_, res, token) => {
|
wrapRequest(async (_, res, token) => {
|
||||||
|
@ -13,11 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { AccountUuid, Ref } from '@hcengineering/core'
|
||||||
import { countTokens } from '@hcengineering/openai'
|
import { countTokens } from '@hcengineering/openai'
|
||||||
import { Tiktoken } from 'js-tiktoken'
|
import { Tiktoken } from 'js-tiktoken'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
import { AccountUuid } from '@hcengineering/core'
|
|
||||||
|
|
||||||
|
import { PersonMessage } from '@hcengineering/ai-bot'
|
||||||
|
import contact, { Contact } from '@hcengineering/contact'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
import { HistoryRecord } from '../types'
|
import { HistoryRecord } from '../types'
|
||||||
import { WorkspaceClient } from '../workspace/workspaceClient'
|
import { WorkspaceClient } from '../workspace/workspaceClient'
|
||||||
@ -25,7 +27,7 @@ import { getTools } from './tools'
|
|||||||
|
|
||||||
export async function translateHtml (client: OpenAI, html: string, lang: string): Promise<string | undefined> {
|
export async function translateHtml (client: OpenAI, html: string, lang: string): Promise<string | undefined> {
|
||||||
const response = await client.chat.completions.create({
|
const response = await client.chat.completions.create({
|
||||||
model: config.OpenAITranslateModel,
|
model: config.OpenAISummaryModel,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@ -41,6 +43,72 @@ export async function translateHtml (client: OpenAI, html: string, lang: string)
|
|||||||
return response.choices[0].message.content ?? undefined
|
return response.choices[0].message.content ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function summarizeMessages (
|
||||||
|
client: OpenAI,
|
||||||
|
messages: PersonMessage[],
|
||||||
|
lang: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const personToName = new Map<Ref<Contact>, string>()
|
||||||
|
for (const m of messages) {
|
||||||
|
if (!personToName.has(m.personRef)) {
|
||||||
|
personToName.set(m.personRef, m.personName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameUsage = new Map<string, number>()
|
||||||
|
for (const [personRef, name] of personToName) {
|
||||||
|
const idx = nameUsage.get(name) ?? 0
|
||||||
|
if (idx > 0) {
|
||||||
|
personToName.set(personRef, name + ` no.${idx}`)
|
||||||
|
}
|
||||||
|
nameUsage.set(name, idx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = messages.map((p) => '---\n\n@' + p.personName + '\n' + p.text).join('\n\n')
|
||||||
|
|
||||||
|
const prompt = `Generate a summary from the provided sequence of messages by creating separate bullet lists for each participant, ensuring that each bullet point includes only the key points, problems and further work plans without any chit-chat, and clearly label each participant so that their individual contributions are distinctly summarized.
|
||||||
|
Use following structure for output:
|
||||||
|
**@Participant Name**
|
||||||
|
- Key point 1
|
||||||
|
- Key point 2
|
||||||
|
- ...
|
||||||
|
**@Participant Name**
|
||||||
|
- Key point 1
|
||||||
|
- ...
|
||||||
|
Don't introduce any other elements of the structure.
|
||||||
|
If a bullet point implies a reference to another participant include a reference according to this format: **@Participant Name**
|
||||||
|
The response should be translated into ${lang} regardless of the original language. Don't translate the names of the participants and leave them exactly as they appear in the text.`
|
||||||
|
|
||||||
|
const response = await client.chat.completions.create({
|
||||||
|
model: config.OpenAIModel,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: prompt
|
||||||
|
},
|
||||||
|
// We could also pack them into separate messages,
|
||||||
|
// but for now it seems that this option is more preferable
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
let responseText = response.choices[0].message.content ?? undefined
|
||||||
|
if (responseText === undefined) return
|
||||||
|
|
||||||
|
const classURI = encodeURIComponent(contact.class.Contact)
|
||||||
|
for (const [personRef, name] of personToName) {
|
||||||
|
const idURI = encodeURIComponent(personRef)
|
||||||
|
const nameURI = encodeURIComponent(name)
|
||||||
|
const refString = `[](ref://?_class=${classURI}&_id=${idURI}&label=${nameURI})`
|
||||||
|
responseText = responseText.replaceAll(`**@${name}**`, refString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
|
|
||||||
export async function createChatCompletion (
|
export async function createChatCompletion (
|
||||||
client: OpenAI,
|
client: OpenAI,
|
||||||
message: OpenAI.ChatCompletionMessageParam,
|
message: OpenAI.ChatCompletionMessageParam,
|
||||||
|
Loading…
Reference in New Issue
Block a user