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

Merge pull request #9413 from weseek/support/import-openai-features-dynamically

support: Import OpenAI features dynamically
mergify[bot] 1 год назад
Родитель
Сommit
0ac74caedb

+ 27 - 9
apps/app/src/features/openai/server/routes/index.ts

@@ -1,18 +1,36 @@
+import { ErrorV3 } from '@growi/core/dist/models';
 import express from 'express';
 
-import { postMessageHandlersFactory } from './message';
-import { rebuildVectorStoreHandlersFactory } from './rebuild-vector-store';
-import { createThreadHandlersFactory } from './thread';
+import type Crowi from '~/server/crowi';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+
+import { isAiEnabled } from '../services';
 
 const router = express.Router();
 
-module.exports = (crowi) => {
-  router.post('/rebuild-vector-store', rebuildVectorStoreHandlersFactory(crowi));
 
-  // create thread
-  router.post('/thread', createThreadHandlersFactory(crowi));
-  // post message and return streaming with SSE
-  router.post('/message', postMessageHandlersFactory(crowi));
+export const factory = (crowi: Crowi): express.Router => {
+
+  // disable all routes if AI is not enabled
+  if (!isAiEnabled()) {
+    router.all('*', (req, res: ApiV3Response) => {
+      return res.apiv3Err(new ErrorV3('GROWI AI is not enabled'), 501);
+    });
+  }
+  // enabled
+  else {
+    import('./rebuild-vector-store').then(({ rebuildVectorStoreHandlersFactory }) => {
+      router.post('/rebuild-vector-store', rebuildVectorStoreHandlersFactory(crowi));
+    });
+
+    import('./thread').then(({ createThreadHandlersFactory }) => {
+      router.post('/thread', createThreadHandlersFactory(crowi));
+    });
+
+    import('./message').then(({ postMessageHandlersFactory }) => {
+      router.post('/message', postMessageHandlersFactory(crowi));
+    });
+  }
 
   return router;
 };

+ 1 - 1
apps/app/src/features/openai/server/routes/message.ts

@@ -14,7 +14,7 @@ import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-respo
 import loggerFactory from '~/utils/logger';
 
 import { MessageErrorCode, type StreamErrorCode } from '../../interfaces/message-error';
-import { openaiClient } from '../services';
+import { openaiClient } from '../services/client';
 import { getStreamErrorCode } from '../services/getStreamErrorCode';
 import { replaceAnnotationWithPageLink } from '../services/replace-annotation-with-page-link';
 

+ 20 - 0
apps/app/src/features/openai/server/services/cron/index.ts

@@ -0,0 +1,20 @@
+import loggerFactory from '~/utils/logger';
+
+import { isAiEnabled } from '../is-ai-enabled';
+
+
+const logger = loggerFactory('growi:openai:service:cron');
+
+export const startCronIfEnabled = async(): Promise<void> => {
+  if (isAiEnabled()) {
+    logger.info('Starting cron service for thread deletion');
+    const { ThreadDeletionCronService } = await import('./thread-deletion-cron');
+    const threadDeletionCronService = new ThreadDeletionCronService();
+    threadDeletionCronService.startCron();
+
+    logger.info('Starting cron service for vector store file deletion');
+    const { VectorStoreFileDeletionCronService } = await import('./vector-store-file-deletion-cron');
+    const vectorStoreFileDeletionCronService = new VectorStoreFileDeletionCronService();
+    vectorStoreFileDeletionCronService.startCron();
+  }
+};

+ 5 - 6
apps/app/src/features/openai/server/services/thread-deletion-cron.ts → apps/app/src/features/openai/server/services/cron/thread-deletion-cron.ts

@@ -4,11 +4,13 @@ import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 
-import { getOpenaiService, type IOpenaiService } from './openai';
+import { isAiEnabled } from '../is-ai-enabled';
+import { getOpenaiService, type IOpenaiService } from '../openai';
+
 
 const logger = loggerFactory('growi:service:thread-deletion-cron');
 
-class ThreadDeletionCronService {
+export class ThreadDeletionCronService {
 
   cronJob: nodeCron.ScheduledTask;
 
@@ -25,8 +27,7 @@ class ThreadDeletionCronService {
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
 
   startCron(): void {
-    const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
-    if (!isAiEnabled) {
+    if (!isAiEnabled()) {
       return;
     }
 
@@ -67,5 +68,3 @@ class ThreadDeletionCronService {
   }
 
 }
-
-export default ThreadDeletionCronService;

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

@@ -4,11 +4,12 @@ import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 
-import { getOpenaiService, type IOpenaiService } from './openai';
+import { isAiEnabled } from '../is-ai-enabled';
+import { getOpenaiService, type IOpenaiService } from '../openai';
 
 const logger = loggerFactory('growi:service:vector-store-file-deletion-cron');
 
-class VectorStoreFileDeletionCronService {
+export class VectorStoreFileDeletionCronService {
 
   cronJob: nodeCron.ScheduledTask;
 
@@ -25,8 +26,7 @@ class VectorStoreFileDeletionCronService {
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
 
   startCron(): void {
-    const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
-    if (!isAiEnabled) {
+    if (!isAiEnabled()) {
       return;
     }
 
@@ -67,5 +67,3 @@ class VectorStoreFileDeletionCronService {
   }
 
 }
-
-export default VectorStoreFileDeletionCronService;

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

@@ -1,2 +1 @@
-export * from './embeddings';
-export * from './client';
+export * from './is-ai-enabled';

+ 3 - 0
apps/app/src/features/openai/server/services/is-ai-enabled.ts

@@ -0,0 +1,3 @@
+import { configManager } from '~/server/service/config-manager';
+
+export const isAiEnabled = (): boolean => configManager.getConfig('crowi', 'app:aiEnabled');

+ 2 - 7
apps/app/src/server/crowi/index.js

@@ -12,8 +12,7 @@ import pkg from '^/package.json';
 
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
-import OpenaiThreadDeletionCronService from '~/features/openai/server/services/thread-deletion-cron';
-import OpenaiVectorStoreFileDeletionCronService from '~/features/openai/server/services/vector-store-file-deletion-cron';
+import { startCronIfEnabled as startOpenaiCronIfEnabled } from '~/features/openai/server/services/cron';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import loggerFactory from '~/utils/logger';
@@ -326,11 +325,7 @@ Crowi.prototype.setupCron = function() {
   this.questionnaireCronService = new QuestionnaireCronService(this);
   this.questionnaireCronService.startCron();
 
-  this.openaiThreadDeletionCronService = new OpenaiThreadDeletionCronService();
-  this.openaiThreadDeletionCronService.startCron();
-
-  this.openaiThreadDeletionCronService = new OpenaiVectorStoreFileDeletionCronService();
-  this.openaiThreadDeletionCronService.startCron();
+  startOpenaiCronIfEnabled();
 };
 
 Crowi.prototype.setupQuestionnaireService = function() {

+ 2 - 2
apps/app/src/server/routes/apiv3/index.js

@@ -1,5 +1,5 @@
 import growiPlugin from '~/features/growi-plugin/server/routes/apiv3/admin';
-import openai from '~/features/openai/server/routes';
+import { factory as openaiRouteFactory } from '~/features/openai/server/routes';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -120,7 +120,7 @@ module.exports = (crowi, app) => {
   router.use('/questionnaire', require('~/features/questionnaire/server/routes/apiv3/questionnaire')(crowi));
   router.use('/templates', require('~/features/templates/server/routes/apiv3')(crowi));
 
-  router.use('/openai', openai(crowi));
+  router.use('/openai', openaiRouteFactory(crowi));
 
   return [router, routerForAdmin, routerForAuth];
 };

+ 10 - 7
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -11,7 +11,7 @@ import { body } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
-import { getOpenaiService } from '~/features/openai/server/services/openai';
+import { isAiEnabled } from '~/features/openai/server/services';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
@@ -202,12 +202,15 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     }
 
     // Rebuild vector store file
-    try {
-      const openaiService = getOpenaiService();
-      await openaiService?.rebuildVectorStore(createdPage);
-    }
-    catch (err) {
-      logger.error('Rebuild vector store failed', err);
+    if (isAiEnabled()) {
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
+      try {
+        const openaiService = getOpenaiService();
+        await openaiService?.rebuildVectorStore(createdPage);
+      }
+      catch (err) {
+        logger.error('Rebuild vector store failed', err);
+      }
     }
   }
 

+ 10 - 7
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -11,7 +11,7 @@ import { body } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
-import { getOpenaiService } from '~/features/openai/server/services/openai';
+import { isAiEnabled } from '~/features/openai/server/services';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
@@ -118,12 +118,15 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     }
 
     // Rebuild vector store file
-    try {
-      const openaiService = getOpenaiService();
-      await openaiService?.rebuildVectorStore(updatedPage);
-    }
-    catch (err) {
-      logger.error('Rebuild vector store failed', err);
+    if (isAiEnabled()) {
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
+      try {
+        const openaiService = getOpenaiService();
+        await openaiService?.rebuildVectorStore(updatedPage);
+      }
+      catch (err) {
+        logger.error('Rebuild vector store failed', err);
+      }
     }
   }
 

+ 24 - 12
apps/app/src/server/service/page/index.ts

@@ -23,7 +23,7 @@ import streamToPromise from 'stream-to-promise';
 import { Comment } from '~/features/comment/server';
 import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
-import { getOpenaiService } from '~/features/openai/server/services/openai';
+import { isAiEnabled } from '~/features/openai/server/services';
 import { SupportedAction } from '~/interfaces/activity';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
@@ -1175,9 +1175,13 @@ class PageService implements IPageService {
         newPagePath, populatedPage?.revision?.body ?? '', user, options,
       );
 
-      // Do not await because communication with OpenAI takes time
-      const openaiService = getOpenaiService();
-      openaiService?.createVectorStoreFile([duplicatedTarget]);
+      if (isAiEnabled()) {
+        const { getOpenaiService } = await import('~/features/openai/server/services/openai');
+
+        // Do not await because communication with OpenAI takes time
+        const openaiService = getOpenaiService();
+        openaiService?.createVectorStoreFile([duplicatedTarget]);
+      }
     }
     this.pageEvent.emit('duplicate', page, user);
 
@@ -1412,9 +1416,13 @@ class PageService implements IPageService {
     const duplicatedPagesWithPopulatedToShowRevison = await Page
       .find({ _id: { $in: duplicatedPageIds }, grant: PageGrant.GRANT_PUBLIC }).populate('revision') as PageDocument[];
 
-    // Do not await because communication with OpenAI takes time
-    const openaiService = getOpenaiService();
-    openaiService?.createVectorStoreFile(duplicatedPagesWithPopulatedToShowRevison);
+    if (isAiEnabled()) {
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
+
+      // Do not await because communication with OpenAI takes time
+      const openaiService = getOpenaiService();
+      openaiService?.createVectorStoreFile(duplicatedPagesWithPopulatedToShowRevison);
+    }
   }
 
   private async duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix) {
@@ -1901,11 +1909,15 @@ class PageService implements IPageService {
       // Leave bookmarks without deleting -- 2024.05.17 Yuki Takei
     ]);
 
-    const openaiService = getOpenaiService();
-    if (openaiService != null) {
-      const vectorStore = await openaiService.getOrCreateVectorStoreForPublicScope();
-      const deleteVectorStoreFilePromises = pageIds.map(pageId => openaiService.deleteVectorStoreFile(vectorStore._id, pageId));
-      await Promise.allSettled(deleteVectorStoreFilePromises);
+    if (isAiEnabled()) {
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
+
+      const openaiService = getOpenaiService();
+      if (openaiService != null) {
+        const vectorStore = await openaiService.getOrCreateVectorStoreForPublicScope();
+        const deleteVectorStoreFilePromises = pageIds.map(pageId => openaiService.deleteVectorStoreFile(vectorStore._id, pageId));
+        await Promise.allSettled(deleteVectorStoreFilePromises);
+      }
     }
   }