Explorar o código

Merge pull request #9718 from weseek/feat/162669-delete-associated-ai-assistants-on-user-deletion

feat: Delete associated ai assistants on user deletion
Shun Miyazawa hai 1 ano
pai
achega
035c6b3b96

+ 2 - 7
apps/app/src/features/openai/server/routes/delete-ai-assistant.ts

@@ -11,7 +11,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { getOpenaiService } from '../services/openai';
+import { deleteAiAssistant } from '../services/delete-ai-assistant';
 
 import { certifyAiService } from './middlewares/certify-ai-service';
 
@@ -41,13 +41,8 @@ export const deleteAiAssistantsFactory: DeleteAiAssistantsFactory = (crowi) => {
       const { id } = req.params;
       const { user } = req;
 
-      const openaiService = getOpenaiService();
-      if (openaiService == null) {
-        return res.apiv3Err(new ErrorV3('GROWI AI is not enabled'), 501);
-      }
-
       try {
-        const deletedAiAssistant = await openaiService.deleteAiAssistant(user._id, id);
+        const deletedAiAssistant = await deleteAiAssistant(user._id, id);
         return res.apiv3({ deletedAiAssistant });
       }
       catch (err) {

+ 1 - 1
apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts

@@ -47,7 +47,7 @@ export class VectorStoreFileDeletionCronService {
   }
 
   private async executeJob(): Promise<void> {
-    await this.openaiService.deleteObsolatedVectorStoreRelations();
+    await this.openaiService.deleteObsoletedVectorStoreRelations();
     await this.openaiService.deleteObsoleteVectorStoreFile(this.vectorStoreFileDeletionBarchSize, this.vectorStoreFileDeletionApiCallInterval);
   }
 

+ 51 - 0
apps/app/src/features/openai/server/services/delete-ai-assistant.ts

@@ -0,0 +1,51 @@
+import {
+  getIdStringForRef, type IUserHasId,
+} from '@growi/core';
+import createError from 'http-errors';
+
+import loggerFactory from '~/utils/logger';
+
+import type { AiAssistantDocument } from '../models/ai-assistant';
+import AiAssistantModel from '../models/ai-assistant';
+
+import { isAiEnabled } from './is-ai-enabled';
+import { getOpenaiService } from './openai';
+
+const logger = loggerFactory('growi:service:openai:delete-ai-assistant');
+
+
+export const deleteAiAssistant = async(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument> => {
+  const openaiService = getOpenaiService();
+  if (openaiService == null) {
+    throw createError('openaiService is not initialized', 500);
+  }
+
+  const aiAssistant = await AiAssistantModel.findOne({ owner: ownerId, _id: aiAssistantId });
+  if (aiAssistant == null) {
+    throw createError(404, 'AiAssistant document does not exist');
+  }
+
+  const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
+  await openaiService.deleteVectorStore(vectorStoreRelationId);
+
+  const deletedAiAssistant = await aiAssistant.remove();
+  return deletedAiAssistant;
+};
+
+export const deleteUserAiAssistant = async(user: IUserHasId): Promise<void> => {
+  if (isAiEnabled()) {
+    const aiAssistants = await AiAssistantModel.find({ owner: user });
+    for await (const aiAssistant of aiAssistants) {
+      try {
+        await deleteAiAssistant(user._id, aiAssistant._id);
+      }
+      catch (err) {
+        logger.error(`Failed to delete AiAssistant ${aiAssistant._id}`);
+      }
+    }
+  }
+
+  // Cannot delete OpenAI VectorStore entities without enabling openaiService.
+  // Delete OpenAI VectorStore entities through "deleteVectorStoresOrphanedFromAiAssistant" when app starts with openaiService enabled
+  await AiAssistantModel.deleteMany({ owner: user });
+};

+ 2 - 16
apps/app/src/features/openai/server/services/openai.ts

@@ -69,7 +69,7 @@ export interface IOpenaiService {
   getThreadsByAiAssistantId(aiAssistantId: string): Promise<ThreadRelationDocument[]>
   deleteThread(threadRelationId: string): Promise<ThreadRelationDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
-  deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
+  deleteObsoletedVectorStoreRelations(): Promise<void> // for CronJob
   deleteVectorStore(vectorStoreRelationId: string): Promise<void>;
   getMessageData(threadId: string, lang?: Lang, options?: MessageListParams): Promise<OpenAI.Beta.Threads.Messages.MessagesPage>;
   createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
@@ -82,7 +82,6 @@ export interface IOpenaiService {
   createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
   updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
   getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
-  deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument>
   isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean>;
 }
 class OpenaiService implements IOpenaiService {
@@ -387,7 +386,7 @@ class OpenaiService implements IOpenaiService {
   }
 
   // Deletes all VectorStore documents that are marked as deleted (isDeleted: true) and have no associated VectorStoreFileRelation documents
-  async deleteObsolatedVectorStoreRelations(): Promise<void> {
+  async deleteObsoletedVectorStoreRelations(): Promise<void> {
     const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
     if (deletedVectorStoreRelations.length === 0) {
       return;
@@ -878,19 +877,6 @@ class OpenaiService implements IOpenaiService {
     };
   }
 
-  async deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument> {
-    const aiAssistant = await AiAssistantModel.findOne({ owner: ownerId, _id: aiAssistantId });
-    if (aiAssistant == null) {
-      throw createError(404, 'AiAssistant document does not exist');
-    }
-
-    const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
-    await this.deleteVectorStore(vectorStoreRelationId);
-
-    const deletedAiAssistant = await aiAssistant.remove();
-    return deletedAiAssistant;
-  }
-
   async isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean> {
     const normalizedPagePathPatterns = removeGlobPath(pagePathPatterns);
 

+ 3 - 0
apps/app/src/server/routes/apiv3/users.js

@@ -9,6 +9,7 @@ import { body, query } from 'express-validator';
 import { isEmail } from 'validator';
 
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import { deleteUserAiAssistant } from '~/features/openai/server/services/delete-ai-assistant';
 import { SupportedAction } from '~/interfaces/activity';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
@@ -809,6 +810,8 @@ module.exports = (crowi) => {
       await user.statusDelete();
       await ExternalAccount.remove({ user });
 
+      deleteUserAiAssistant(user);
+
       const serializedUser = serializeUserSecurely(user);
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });

+ 11 - 3
apps/app/src/server/service/normalize-data/delete-legacy-knowledge-assistant-vector-store.ts → apps/app/src/server/service/normalize-data/delete-vector-stores-orphaned-from-ai-assistant.ts

@@ -2,8 +2,11 @@ import AiAssistantModel from '~/features/openai/server/models/ai-assistant';
 import VectorStoreRelationModel from '~/features/openai/server/models/vector-store';
 import { isAiEnabled } from '~/features/openai/server/services/is-ai-enabled';
 import { getOpenaiService } from '~/features/openai/server/services/openai';
+import loggerFactory from '~/utils/logger';
 
-export const deleteLegacyKnowledgeAssistantVectorStore = async(): Promise<void> => {
+const logger = loggerFactory('growi:service:normalize-data:delete-vector-stores-orphaned-from-ai-assistant');
+
+export const deleteVectorStoresOrphanedFromAiAssistant = async(): Promise<void> => {
   if (!isAiEnabled()) {
     return;
   }
@@ -20,7 +23,12 @@ export const deleteLegacyKnowledgeAssistantVectorStore = async(): Promise<void>
   // Logically delete only the VectorStore entities, leaving related documents to be automatically deleted by cron job
   const openaiService = getOpenaiService();
   for await (const vectorStoreRelation of nonDeletedLegacyKnowledgeAssistantVectorStoreRelations) {
-    const vectorStoreFileRelationId = vectorStoreRelation._id;
-    await openaiService?.deleteVectorStore(vectorStoreFileRelationId);
+    try {
+      const vectorStoreFileRelationId = vectorStoreRelation._id;
+      await openaiService?.deleteVectorStore(vectorStoreFileRelationId);
+    }
+    catch (err) {
+      logger.error(err);
+    }
   }
 };

+ 2 - 2
apps/app/src/server/service/normalize-data/index.ts

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 
 import { convertNullToEmptyGrantedArrays } from './convert-null-to-empty-granted-arrays';
 import { convertRevisionPageIdToObjectId } from './convert-revision-page-id-to-objectid';
-import { deleteLegacyKnowledgeAssistantVectorStore } from './delete-legacy-knowledge-assistant-vector-store';
+import { deleteVectorStoresOrphanedFromAiAssistant } from './delete-vector-stores-orphaned-from-ai-assistant';
 import { renameDuplicateRootPages } from './rename-duplicate-root-pages';
 
 const logger = loggerFactory('growi:service:NormalizeData');
@@ -13,7 +13,7 @@ export const normalizeData = async(): Promise<void> => {
   await convertRevisionPageIdToObjectId();
   await normalizeExpiredAtForThreadRelations();
   await convertNullToEmptyGrantedArrays();
-  await deleteLegacyKnowledgeAssistantVectorStore();
+  await deleteVectorStoresOrphanedFromAiAssistant();
 
   logger.info('normalizeData has been executed');
   return;