mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3549 from gradido/frontend_openai_consider_thread_timeouts
fix(backend): check for openai thread timeout
This commit is contained in:
commit
9dc35f8080
@ -15,7 +15,11 @@
|
||||
</div>
|
||||
<div ref="chatContainer" class="messages-scroll-container">
|
||||
<TransitionGroup class="messages" tag="div" name="chat">
|
||||
<div v-for="(message, index) in messages" :key="index" :class="['message', message.role]">
|
||||
<div
|
||||
v-for="(message, index) in messages"
|
||||
:key="index"
|
||||
:class="['message', message.role, { 'message-error': message.isError }]"
|
||||
>
|
||||
<div class="message-content position-relative inner-container">
|
||||
<span v-html="formatMessage(message)"></span>
|
||||
<b-button
|
||||
@ -170,7 +174,18 @@ const sendMessage = () => {
|
||||
onMounted(async () => {
|
||||
if (messages.value.length === 0) {
|
||||
loading.value = true
|
||||
await resumeChatRefetch()
|
||||
try {
|
||||
await resumeChatRefetch()
|
||||
} catch (error) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
toastError(`Error loading chat: ${error.graphQLErrors[0].message}`)
|
||||
return
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify(error, null, 2))
|
||||
toastError(`Error loading chat: ${error}`)
|
||||
}
|
||||
}
|
||||
const messagesFromServer = resumeChatResult.value.resumeChat
|
||||
if (messagesFromServer && messagesFromServer.length > 0) {
|
||||
threadId.value = messagesFromServer[0].threadId
|
||||
@ -279,6 +294,17 @@ onMounted(async () => {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.error .message-content {
|
||||
background-color: #f1e5e5;
|
||||
color: rgb(194 12 12);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
|
||||
@ -26,6 +26,7 @@ fragment AiChatMessageFields on ChatGptMessage {
|
||||
content
|
||||
role
|
||||
threadId
|
||||
isError
|
||||
}
|
||||
|
||||
fragment UserCommonFields on User {
|
||||
|
||||
@ -11,6 +11,8 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.openai.OpenaiClient`)
|
||||
// this is the time after when openai is deleting an inactive thread
|
||||
const OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS = 60
|
||||
|
||||
/**
|
||||
* The `OpenaiClient` class is a singleton that provides an interface to interact with the OpenAI API.
|
||||
@ -87,21 +89,35 @@ export class OpenaiClient {
|
||||
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()
|
||||
if (openaiThreadEntity.updatedAt < new Date(Date.now() - OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS * 24 * 60 * 60 * 1000)) {
|
||||
logger.info(`Openai thread for user: ${user.id} is older than ${OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS} days, deleting...`)
|
||||
// let run async, because it could need some time, but we don't need to wait, because we create a new one nevertheless
|
||||
void this.deleteThread(openaiThreadEntity.id)
|
||||
return []
|
||||
}
|
||||
try {
|
||||
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()
|
||||
logger.info(`Resumed thread: ${openaiThreadEntity.id}`)
|
||||
return threadMessages
|
||||
.map(
|
||||
(message) =>
|
||||
new MessageModel(
|
||||
this.messageContentToString(message),
|
||||
message.role,
|
||||
openaiThreadEntity.id,
|
||||
),
|
||||
)
|
||||
.reverse()
|
||||
} catch (e) {
|
||||
if(e instanceof Error && e.toString().includes('No thread found with id')) {
|
||||
logger.info(`Thread not found: ${openaiThreadEntity.id}`)
|
||||
return []
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteThread(threadId: string): Promise<boolean> {
|
||||
@ -124,6 +140,7 @@ export class OpenaiClient {
|
||||
}
|
||||
|
||||
public async runAndGetLastNewMessage(threadId: string): Promise<MessageModel> {
|
||||
const updateOpenAiThreadResolver = OpenaiThreads.update({ id: threadId }, { updatedAt: new Date() })
|
||||
const run = await this.openai.beta.threads.runs.createAndPoll(threadId, {
|
||||
assistant_id: CONFIG.OPENAI_ASSISTANT_ID,
|
||||
})
|
||||
@ -138,6 +155,7 @@ export class OpenaiClient {
|
||||
logger.warn(`No message in thread: ${threadId}, run: ${run.id}`, messagesPage.data)
|
||||
return new MessageModel('No Answer', 'assistant')
|
||||
}
|
||||
await updateOpenAiThreadResolver
|
||||
return new MessageModel(this.messageContentToString(message), 'assistant')
|
||||
}
|
||||
|
||||
|
||||
@ -10,10 +10,14 @@ export class ChatGptMessage {
|
||||
@Field()
|
||||
role: string
|
||||
|
||||
@Field()
|
||||
threadId: string
|
||||
@Field({ nullable: true })
|
||||
threadId?: string
|
||||
|
||||
public constructor(data: Partial<Message>) {
|
||||
@Field()
|
||||
isError: boolean
|
||||
|
||||
public constructor(data: Partial<Message>, isError: boolean = false) {
|
||||
Object.assign(this, data)
|
||||
this.isError = isError
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,10 +15,10 @@ export class AiChatResolver {
|
||||
async resumeChat(@Ctx() context: Context): Promise<ChatGptMessage[]> {
|
||||
const openaiClient = OpenaiClient.getInstance()
|
||||
if (!openaiClient) {
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'OpenAI API is not enabled' })])
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'OpenAI API is not enabled', role: 'assistant' }, true)])
|
||||
}
|
||||
if (!context.user) {
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'User not found' })])
|
||||
return Promise.resolve([new ChatGptMessage({ content: 'User not found', role: 'assistant' }, true)])
|
||||
}
|
||||
const messages = await openaiClient.resumeThread(context.user)
|
||||
return messages.map((message) => new ChatGptMessage(message))
|
||||
@ -42,10 +42,10 @@ export class AiChatResolver {
|
||||
): Promise<ChatGptMessage> {
|
||||
const openaiClient = OpenaiClient.getInstance()
|
||||
if (!openaiClient) {
|
||||
return Promise.resolve(new ChatGptMessage({ content: 'OpenAI API is not enabled' }))
|
||||
return Promise.resolve(new ChatGptMessage({ content: 'OpenAI API is not enabled', role: 'assistant' }, true))
|
||||
}
|
||||
if (!context.user) {
|
||||
return Promise.resolve(new ChatGptMessage({ content: 'User not found' }))
|
||||
return Promise.resolve(new ChatGptMessage({ content: 'User not found', role: 'assistant' }, true))
|
||||
}
|
||||
const messageObj = new Message(message)
|
||||
if (!threadId || threadId.length === 0) {
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `openai_threads` ADD COLUMN `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP AFTER `createdAt`;'
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `openai_threads` DROP COLUMN `updatedAt`;')
|
||||
}
|
||||
@ -1,13 +1,16 @@
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
@Entity('openai_threads')
|
||||
export class OpenaiThreads extends BaseEntity {
|
||||
@PrimaryColumn({ type: 'char', length: 30 })
|
||||
id: string
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updatedAt: Date
|
||||
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true })
|
||||
userId: number
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user