mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 01:46:07 +00:00
improve openai thread management
This commit is contained in:
parent
978aa59e82
commit
33899e5d2b
@ -31,15 +31,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { sendMessage as sendMessageMutation } from '../graphql/aiChat.graphql'
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable'
|
||||
import {
|
||||
sendMessage as sendMessageMutation,
|
||||
resumeChat,
|
||||
deleteThread,
|
||||
} from '../graphql/aiChat.graphql'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toastError } = useAppToast()
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
const response = useMutation(sendMessageMutation, { input: ref('') })
|
||||
const deleteResponse = useMutation(deleteThread, { threadId: ref('') })
|
||||
const { result: resumeChatResult, refetch: resumeChatRefetch } = useQuery(resumeChat)
|
||||
|
||||
const isChatOpen = ref(false)
|
||||
const newMessage = ref('')
|
||||
@ -51,6 +57,21 @@ const toggleButtonVariant = computed(() => (isChatOpen.value ? 'secondary' : 'pr
|
||||
|
||||
const toggleChat = () => {
|
||||
isChatOpen.value = !isChatOpen.value
|
||||
if (!isChatOpen.value && threadId.value && threadId.value.length > 0) {
|
||||
// delete thread on closing chat
|
||||
deleteResponse
|
||||
.mutate({ threadId: threadId.value })
|
||||
.then((result) => {
|
||||
threadId.value = ''
|
||||
messages.value = []
|
||||
if (result.data.deleteThread) {
|
||||
toastSuccess(t('ai.chat-thread-deleted'))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(t('ai.error-chat-thread-deleted', { error }))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
@ -73,6 +94,19 @@ const sendMessage = () => {
|
||||
newMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (messages.value.length === 0) {
|
||||
loading.value = true
|
||||
await resumeChatRefetch()
|
||||
const messagesFromServer = resumeChatResult.value.resumeChat
|
||||
if (messagesFromServer && messagesFromServer.length > 0) {
|
||||
threadId.value = messagesFromServer[0].threadId
|
||||
messages.value = messagesFromServer
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
#import './fragments.graphql'
|
||||
|
||||
mutation sendMessage($input: OpenaiMessage!) {
|
||||
sendMessage(input: $input) {
|
||||
content
|
||||
role
|
||||
threadId
|
||||
...AiChatMessageFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation deleteThread($threadId: String!) {
|
||||
deleteThread(threadId: $threadId)
|
||||
}
|
||||
|
||||
query resumeChat {
|
||||
resumeChat {
|
||||
...AiChatMessageFields
|
||||
}
|
||||
}
|
||||
@ -21,3 +21,9 @@ fragment ProjectBrandingCommonFields on ProjectBranding {
|
||||
newUserToSpace
|
||||
logoUrl
|
||||
}
|
||||
|
||||
fragment AiChatMessageFields on ChatGptMessage {
|
||||
content
|
||||
role
|
||||
threadId
|
||||
}
|
||||
@ -4,7 +4,9 @@
|
||||
"ai": {
|
||||
"chat": "Chat",
|
||||
"chat-open": "Chat öffnen",
|
||||
"chat-placeholder": "Schreibe eine Nachricht..."
|
||||
"chat-placeholder": "Schreibe eine Nachricht...",
|
||||
"chat-thread-deleted": "Chatverlauf gelöscht",
|
||||
"error-chat-thread-deleted": "Fehler beim Löschen des Chatverlaufs: {error}"
|
||||
},
|
||||
"alias": "Alias",
|
||||
"all_emails": "Alle Nutzer",
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
"ai": {
|
||||
"chat": "Chat",
|
||||
"chat-open": "Open chat",
|
||||
"chat-placeholder": "Type your message here..."
|
||||
"chat-placeholder": "Type your message here...",
|
||||
"chat-thread-deleted": "Chat thread has been deleted",
|
||||
"error-chat-thread-deleted": "Error while deleting chat thread: {error}"
|
||||
},
|
||||
"alias": "Alias",
|
||||
"all_emails": "All users",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
JWT_EXPIRES_IN=2m
|
||||
|
||||
GDT_ACTIVE=false
|
||||
OPENAI_ACTIVE=false
|
||||
|
||||
# Email
|
||||
EMAIL=true
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
##################################################################################
|
||||
# BASE ###########################################################################
|
||||
##################################################################################
|
||||
FROM node:18.20.7-alpine3.21 as base
|
||||
FROM node:18.20.7-bookworm-slim as base
|
||||
|
||||
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
"pug": "^3.0.2",
|
||||
"random-bigint": "^0.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^3.3.0",
|
||||
"sodium-native": "^3.4.1",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typed-rest-client": "^1.8.11",
|
||||
"uuid": "^8.3.2",
|
||||
|
||||
@ -71,6 +71,49 @@ export class OpenaiClient {
|
||||
return messageThread.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the last message thread for the given user.
|
||||
* @param user
|
||||
* @returns
|
||||
*/
|
||||
public async resumeThread(user: User): Promise<MessageModel[]> {
|
||||
const openaiThreadEntity = await OpenaiThreads.findOne({
|
||||
where: { userId: user.id },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
if (!openaiThreadEntity) {
|
||||
// logger.warn(`No openai thread found for user: ${user.id}`)
|
||||
return []
|
||||
}
|
||||
const threadMessages = (
|
||||
await this.openai.beta.threads.messages.list(openaiThreadEntity.id, { order: 'desc' })
|
||||
).getPaginatedItems()
|
||||
|
||||
logger.info(`Resumed thread: ${openaiThreadEntity.id}`)
|
||||
return threadMessages
|
||||
.map(
|
||||
(message) =>
|
||||
new MessageModel(
|
||||
this.messageContentToString(message),
|
||||
message.role,
|
||||
openaiThreadEntity.id,
|
||||
),
|
||||
)
|
||||
.reverse()
|
||||
}
|
||||
|
||||
public async deleteThread(threadId: string): Promise<boolean> {
|
||||
const result = await this.openai.beta.threads.del(threadId)
|
||||
if (result.deleted) {
|
||||
await OpenaiThreads.delete({ id: threadId })
|
||||
logger.info(`Deleted thread: ${threadId}`)
|
||||
return true
|
||||
} else {
|
||||
logger.warn(`Failed to delete thread: ${threadId}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public async addMessage(message: MessageModel, threadId: string): Promise<void> {
|
||||
const threadMessages = await this.openai.beta.threads.messages.create(threadId, message)
|
||||
logger.info(`Added message to thread: ${threadMessages.id}`)
|
||||
@ -94,7 +137,7 @@ export class OpenaiClient {
|
||||
return new MessageModel(this.messageContentToString(message), 'assistant')
|
||||
}
|
||||
|
||||
messageContentToString(message: Message): string {
|
||||
private messageContentToString(message: Message): string {
|
||||
if (message.content.length > 1) {
|
||||
logger.warn(`More than one content in message: ${message.id}`, message.content)
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
export class Message {
|
||||
content: string
|
||||
role: 'user' | 'assistant' = 'user'
|
||||
threadId: string
|
||||
threadId?: string
|
||||
|
||||
constructor(content: string, role: 'user' | 'assistant' = 'user') {
|
||||
constructor(content: string, role: 'user' | 'assistant' = 'user', threadId?: string) {
|
||||
this.content = content
|
||||
this.role = role
|
||||
this.threadId = threadId
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Resolver, Mutation, Authorized, Ctx, Arg } from 'type-graphql'
|
||||
import { Resolver, Mutation, Authorized, Ctx, Arg, Query } from 'type-graphql'
|
||||
|
||||
import { OpenaiMessage } from '@input/OpenaiMessage'
|
||||
import { ChatGptMessage } from '@model/ChatGptMessage'
|
||||
@ -10,6 +10,30 @@ import { Context } from '@/server/context'
|
||||
|
||||
@Resolver()
|
||||
export class AiChatResolver {
|
||||
@Authorized([RIGHTS.AI_SEND_MESSAGE])
|
||||
@Query(() => [ChatGptMessage])
|
||||
async resumeChat(@Ctx() context: Context): Promise<ChatGptMessage[]> {
|
||||
const openaiClient = OpenaiClient.getInstance()
|
||||
if (!openaiClient) {
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'OpenAI API is not enabled' })])
|
||||
}
|
||||
if (!context.user) {
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'User not found' })])
|
||||
}
|
||||
const messages = await openaiClient.resumeThread(context.user)
|
||||
return messages.map((message) => new ChatGptMessage(message))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.AI_SEND_MESSAGE])
|
||||
@Mutation(() => Boolean)
|
||||
async deleteThread(@Arg('threadId') threadId: string): Promise<boolean> {
|
||||
const openaiClient = OpenaiClient.getInstance()
|
||||
if (!openaiClient) {
|
||||
return false
|
||||
}
|
||||
return openaiClient.deleteThread(threadId)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.AI_SEND_MESSAGE])
|
||||
@Mutation(() => ChatGptMessage)
|
||||
async sendMessage(
|
||||
|
||||
@ -520,7 +520,6 @@ export class UserResolver {
|
||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||
)
|
||||
}
|
||||
|
||||
// load code
|
||||
const userContact = await DbUserContact.findOneOrFail({
|
||||
where: { emailVerificationCode: code },
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
crypto_pwhash,
|
||||
crypto_shorthash,
|
||||
} from 'sodium-native'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
export const SecretKeyCryptographyCreateKey = (
|
||||
salt: string,
|
||||
@ -21,12 +22,29 @@ export const SecretKeyCryptographyCreateKey = (
|
||||
configLoginAppSecret: Buffer,
|
||||
configLoginServerKey: Buffer,
|
||||
): bigint => {
|
||||
/*
|
||||
console.log('SecretKeyCryptographyCreateKey')
|
||||
const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES)
|
||||
crypto_hash_sha512_init(state)
|
||||
console.log('before salt')
|
||||
console.log('salt as buffer', Buffer.from(salt).toString('hex'))
|
||||
crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
console.log('before configLoginAppSecret')
|
||||
console.log('configLoginAppSecret', configLoginAppSecret.toString('hex'))
|
||||
crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(crypto_hash_sha512_BYTES)
|
||||
console.log('hash buffer size', hash.length)
|
||||
console.log('before final')
|
||||
console.log(typeof crypto_hash_sha512_final)
|
||||
crypto_hash_sha512_final(state, hash)
|
||||
console.log('after final')
|
||||
console.log('hash', hash.toString('hex'))
|
||||
*/
|
||||
const state = createHash('sha512')
|
||||
state.update(Buffer.from(salt))
|
||||
state.update(configLoginAppSecret)
|
||||
const hash = state.digest()
|
||||
console.log('hash', hash.toString('hex'))
|
||||
|
||||
const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES)
|
||||
const opsLimit = 10
|
||||
|
||||
@ -58,6 +58,7 @@ export const SecretKeyCryptographyCreateKey = async (
|
||||
}
|
||||
let result: Promise<bigint>
|
||||
if (encryptionWorkerPool) {
|
||||
console.log('encrypt password with worker')
|
||||
result = (await encryptionWorkerPool.exec('SecretKeyCryptographyCreateKey', [
|
||||
salt,
|
||||
password,
|
||||
@ -65,6 +66,7 @@ export const SecretKeyCryptographyCreateKey = async (
|
||||
configLoginServerKey,
|
||||
])) as Promise<bigint>
|
||||
} else {
|
||||
console.log('encrypt password without worker')
|
||||
result = Promise.resolve(
|
||||
SecretKeyCryptographyCreateKeySync(
|
||||
salt,
|
||||
|
||||
@ -6594,7 +6594,7 @@ slick@^1.12.2:
|
||||
resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
|
||||
integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==
|
||||
|
||||
sodium-native@^3.3.0:
|
||||
sodium-native@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.4.1.tgz#44616c07ccecea15195f553af88b3e574b659741"
|
||||
integrity sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user