Просмотр исходного кода

Merge pull request #9626 from weseek/feat/161373-implement-api-to-delete-ai-assistant

feat: Implement api to delete ai assistant
Yuki Takei 1 год назад
Родитель
Сommit
f28ad1b7d5

+ 5 - 1
apps/app/src/features/openai/client/services/ai-assistant.ts

@@ -1,7 +1,11 @@
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { apiv3Post, apiv3Delete } from '~/client/util/apiv3-client';
 
 import type { IApiv3AiAssistantCreateParams } from '../../interfaces/ai-assistant';
 
 export const createAiAssistant = async(body: IApiv3AiAssistantCreateParams): Promise<void> => {
   await apiv3Post('/openai/ai-assistant', body);
 };
+
+export const deleteAiAssistant = async(id: string): Promise<void> => {
+  await apiv3Delete(`/openai/ai-assistant/${id}`);
+};

+ 54 - 0
apps/app/src/features/openai/server/routes/delete-ai-assistant.ts

@@ -0,0 +1,54 @@
+import { type IUserHasId } from '@growi/core';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+import { type ValidationChain, param } from 'express-validator';
+
+
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+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 { certifyAiService } from './middlewares/certify-ai-service';
+
+const logger = loggerFactory('growi:routes:apiv3:openai:delete-ai-assistants');
+
+
+type DeleteAiAssistantsFactory = (crowi: Crowi) => RequestHandler[];
+
+type ReqParams = {
+  id: string,
+}
+
+type Req = Request<ReqParams, Response, undefined> & {
+  user: IUserHasId,
+}
+
+export const deleteAiAssistantsFactory: DeleteAiAssistantsFactory = (crowi) => {
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+
+  const validator: ValidationChain[] = [
+    param('id').isMongoId().withMessage('aiAssistant id is required'),
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const { id } = req.params;
+      const { user } = req;
+
+      try {
+        const openaiService = getOpenaiService();
+        const deletedAiAssistant = await openaiService?.deleteAiAssistant(user._id, id);
+        return res.apiv3({ deletedAiAssistant });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3('Failed to delete AiAssistants'));
+      }
+    },
+  ];
+};

+ 4 - 0
apps/app/src/features/openai/server/routes/index.ts

@@ -38,6 +38,10 @@ export const factory = (crowi: Crowi): express.Router => {
     import('./ai-assistants').then(({ getAiAssistantsFactory }) => {
       router.get('/ai-assistants', getAiAssistantsFactory(crowi));
     });
+
+    import('./delete-ai-assistant').then(({ deleteAiAssistantsFactory }) => {
+      router.delete('/ai-assistant/:id', deleteAiAssistantsFactory(crowi));
+    });
   }
 
   return router;

+ 1 - 1
apps/app/src/features/openai/server/services/openai-api-error-handler.ts

@@ -14,7 +14,7 @@ type ErrorHandler = {
   notFoundError?: () => Promise<void>;
 }
 
-export const oepnaiApiErrorHandler = async(error: unknown, handler: ErrorHandler): Promise<void> => {
+export const openaiApiErrorHandler = async(error: unknown, handler: ErrorHandler): Promise<void> => {
   if (!(error instanceof OpenAI.APIError)) {
     return;
   }

+ 32 - 19
apps/app/src/features/openai/server/services/openai.ts

@@ -3,7 +3,7 @@ import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 
 import {
-  PageGrant, getIdForRef, isPopulated, type IUserHasId,
+  PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
 } from '@growi/core';
 import { isGrobPatternPath } from '@growi/core/dist/utils/page-path-utils';
 import escapeStringRegexp from 'escape-string-regexp';
@@ -32,7 +32,7 @@ import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
 
 import { getClient } from './client-delegator';
 // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
-import { oepnaiApiErrorHandler } from './openai-api-error-handler';
+import { openaiApiErrorHandler } from './openai-api-error-handler';
 
 
 const BATCH_SIZE = 100;
@@ -69,6 +69,7 @@ export interface IOpenaiService {
   // rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
   createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
   getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
+  deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument>
 }
 class OpenaiService implements IOpenaiService {
 
@@ -105,7 +106,7 @@ class OpenaiService implements IOpenaiService {
       return thread;
     }
     catch (err) {
-      await oepnaiApiErrorHandler(err, { notFoundError: async() => { await threadRelation.remove() } });
+      await openaiApiErrorHandler(err, { notFoundError: async() => { await threadRelation.remove() } });
       throw new Error(err);
     }
   }
@@ -205,22 +206,21 @@ class OpenaiService implements IOpenaiService {
     return uploadedFile;
   }
 
-  // TODO: https://redmine.weseek.co.jp/issues/160333
-  // private async deleteVectorStore(vectorStoreScopeType: VectorStoreScopeType): Promise<void> {
-  //   const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: vectorStoreScopeType, isDeleted: false });
-  //   if (vectorStoreDocument == null) {
-  //     return;
-  //   }
+  private async deleteVectorStore(vectorStoreRelationId: string): Promise<void> {
+    const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ _id: vectorStoreRelationId, isDeleted: false });
+    if (vectorStoreDocument == null) {
+      return;
+    }
 
-  //   try {
-  //     await this.client.deleteVectorStore(vectorStoreDocument.vectorStoreId);
-  //     await vectorStoreDocument.markAsDeleted();
-  //   }
-  //   catch (err) {
-  //     await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
-  //     throw new Error(err);
-  //   }
-  // }
+    try {
+      await this.client.deleteVectorStore(vectorStoreDocument.vectorStoreId);
+      await vectorStoreDocument.markAsDeleted();
+    }
+    catch (err) {
+      await openaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
+      throw new Error(err);
+    }
+  }
 
   async createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
     // const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
@@ -328,7 +328,7 @@ class OpenaiService implements IOpenaiService {
         }
       }
       catch (err) {
-        await oepnaiApiErrorHandler(err, { notFoundError: async() => { deletedFileIds.push(fileId) } });
+        await openaiApiErrorHandler(err, { notFoundError: async() => { deletedFileIds.push(fileId) } });
         logger.error(err);
       }
     }
@@ -605,6 +605,19 @@ 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 new Error('AiAssistant document does not exist');
+    }
+
+    const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
+    await this.deleteVectorStore(vectorStoreRelationId);
+
+    const deletedAiAssistant = await aiAssistant.remove();
+    return deletedAiAssistant;
+  }
+
 }
 
 let instance: OpenaiService;