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

Merge pull request #9133 from weseek/feat/153986-implement-api-for-regenerating-vector-store

Shun Miyazawa 1 год назад
Родитель
Сommit
183bff91d8

+ 8 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -1135,6 +1135,13 @@
   },
   "ai_integration": {
     "ai_integration": "AI Integration",
-    "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, please set the environment variable <code>AI_ENABLED</code> to true."
+    "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, please set the environment variable <code>AI_ENABLED</code> to true.",
+    "ai_search_management": "AI search management",
+    "rebuild_vector_store": "Rebuild Vector Store",
+    "rebuild_vector_store_label": "Rebuild",
+    "rebuild_vector_store_explanation1": "Delete the existing Vector Store and recreate the Vector Store on the public page.",
+    "rebuild_vector_store_explanation2": "This process may take several minutes.",
+    "rebuild_vector_store_requested": "Vector Store rebuild has been requested",
+    "rebuild_vector_store_failed": "Vector Store rebuild failed"
   }
 }

+ 8 - 1
apps/app/public/static/locales/fr_FR/admin.json

@@ -1134,6 +1134,13 @@
   },
   "ai_integration": {
     "ai_integration": "Intégration de l'IA",
-    "disable_mode_explanation": "Actuellement, l'intégration de l'IA est désactivée. Pour l'activer, veuillez définir la variable d'environnement <code>AI_ENABLED</code> sur true"
+    "disable_mode_explanation": "Actuellement, l'intégration de l'IA est désactivée. Pour l'activer, veuillez définir la variable d'environnement <code>AI_ENABLED</code> sur true",
+    "ai_search_management": "Gestion de la recherche par l'IA",
+    "rebuild_vector_store": "Reconstruire le magasin Vector",
+    "rebuild_vector_store_label": "Reconstruire",
+    "rebuild_vector_store_explanation1": "Supprimez le Vector Store existant et recréez le Vector Store sur la page publique.",
+    "rebuild_vector_store_explanation2": "Ce processus peut prendre plusieurs minutes.",
+    "rebuild_vector_store_requested": "La reconstruction du magasin Vector a été demandée",
+    "rebuild_vector_store_failed": "Échec de la reconstruction du magasin de vecteurs"
   }
 }

+ 8 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -1145,6 +1145,13 @@
   },
   "ai_integration": {
     "ai_integration": "AI 連携",
-    "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> を true に設定してください。"
+    "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> を true に設定してください。",
+    "ai_search_management": "AI 検索管理",
+    "rebuild_vector_store": "Vector Store のリビルド",
+    "rebuild_vector_store_label": "リビルド",
+    "rebuild_vector_store_explanation1": "既存の Vector Store を削除し、公開ページの Vector Store を再作成します。",
+    "rebuild_vector_store_explanation2": "この作業には数分かかる可能性があります。",
+    "rebuild_vector_store_requested": "Vector Store のリビルドを受け付けました",
+    "rebuild_vector_store_failed": "Vector Store のリビルドに失敗しました"
   }
 }

+ 8 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -1144,6 +1144,13 @@
   },
   "ai_integration": {
     "ai_integration": "AI 集成",
-    "disable_mode_explanation": "目前,AI 集成已禁用。要启用它,请将环境变量 <code>AI_ENABLED</code> 设置为 true”"
+    "disable_mode_explanation": "目前,AI 集成已禁用。要启用它,请将环境变量 <code>AI_ENABLED</code> 设置为 true",
+    "ai_search_management": "AI 搜索管理",
+    "rebuild_vector_store": "重建矢量商店",
+    "rebuild_vector_store_label": "重建",
+    "rebuild_vector_store_explanation1": "删除现有的矢量存储,在公共页面上重新创建矢量存储。",
+    "rebuild_vector_store_explanation2": "这个过程可能需要几分钟。",
+    "rebuild_vector_store_requested": "已要求重建矢量存储库",
+    "rebuild_vector_store_failed": "向量存储区重建失败"
   }
 }

+ 46 - 0
apps/app/src/client/components/Admin/AiIntegration/AiIntegration.tsx

@@ -0,0 +1,46 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+
+
+export const AiIntegration = (): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  const clickRebuildVectorStoreButtonHandler = useCallback(async() => {
+    try {
+      toastSuccess(t('ai_integration.rebuild_vector_store_requested'));
+      await apiv3Post('/openai/rebuild-vector-store');
+    }
+    catch {
+      toastError(t('ai_integration.rebuild_vector_store_failed'));
+    }
+  }, [t]);
+
+  return (
+    <div data-testid="admin-ai-integration">
+      <h2 className="admin-setting-header">{ t('ai_integration.ai_search_management') }</h2>
+
+      <div className="row">
+        <label className="col-md-3 col-form-label text-start text-md-end">{ t('ai_integration.rebuild_vector_store_label') }</label>
+        <div className="col-md-8">
+          {/* TODO: https://redmine.weseek.co.jp/issues/153978 */}
+          <button
+            type="submit"
+            className="btn btn-primary"
+            onClick={clickRebuildVectorStoreButtonHandler}
+          >
+            {t('ai_integration.rebuild_vector_store')}
+          </button>
+
+          <p className="form-text text-muted">
+            {t('ai_integration.rebuild_vector_store_explanation1')}<br />
+            {t('ai_integration.rebuild_vector_store_explanation2')}<br />
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 5 - 0
apps/app/src/interfaces/ai.ts

@@ -0,0 +1,5 @@
+export const aiServiceType = {
+  OPEN_AI: 'openai',
+} as const;
+
+export const aiServiceTypes = Object.values(aiServiceType);

+ 3 - 2
apps/app/src/pages/admin/ai-integration.page.tsx

@@ -13,6 +13,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
+const AiIntegration = dynamic(() => import('~/client/components/Admin/AiIntegration/AiIntegration').then(mod => mod.AiIntegration), { ssr: false });
 const AiIntegrationDisableMode = dynamic(
   () => import('~/client/components/Admin/AiIntegration/AiIntegrationDisableMode').then(mod => mod.AiIntegrationDisableMode), { ssr: false },
 );
@@ -21,7 +22,7 @@ type Props = CommonProps & {
   aiEnabled: boolean,
 };
 
-const AdminAiIntegrationPage: NextPage<Props> = (props) => {
+const AdminAiIntegrationPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation('admin');
 
   const title = t('ai_integration.ai_integration');
@@ -37,7 +38,7 @@ const AdminAiIntegrationPage: NextPage<Props> = (props) => {
         <title>{headTitle}</title>
       </Head>
       {props.aiEnabled
-        ? <></> // TODO: implement admin page
+        ? <AiIntegration />
         : <AiIntegrationDisableMode />
       }
     </AdminLayout>

+ 26 - 0
apps/app/src/server/middlewares/certify-ai-service.ts

@@ -0,0 +1,26 @@
+import type { NextFunction, Request, Response } from 'express';
+
+import { aiServiceTypes } from '~/interfaces/ai';
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:middlewares:certify-ai-service');
+
+export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
+  const aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+  const aiServiceType = configManager.getConfig('crowi', 'app:aiServiceType');
+
+  if (!aiEnabled) {
+    const message = 'AI_ENABLED is not true';
+    logger.error(message);
+    return res.apiv3Err(message, 403);
+  }
+
+  if (aiServiceType == null || !aiServiceTypes.includes(aiServiceType)) {
+    const message = 'AI_SERVICE_TYPE is missing or contains an invalid value';
+    logger.error(message);
+    return res.apiv3Err(message, 403);
+  }
+
+  next();
+};

+ 4 - 2
apps/app/src/server/routes/apiv3/openai/chat.ts

@@ -3,12 +3,14 @@ import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
 
 import type Crowi from '~/server/crowi';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import { certifyAiService } from '~/server/middlewares/certify-ai-service';
 import { configManager } from '~/server/service/config-manager';
 import { openaiClient } from '~/server/service/openai';
 import { getOrCreateChatAssistant } from '~/server/service/openai/assistant';
 import loggerFactory from '~/utils/logger';
 
-import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
 const logger = loggerFactory('growi:routes:apiv3:openai:chat');
@@ -32,7 +34,7 @@ export const chatHandlersFactory: ChatHandlersFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser, loginRequiredStrictly, validator, apiV3FormValidator,
+    accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const vectorStoreId = configManager.getConfig('crowi', 'app:openaiVectorStoreId');
       if (vectorStoreId == null) {

+ 2 - 0
apps/app/src/server/routes/apiv3/openai/index.ts

@@ -1,10 +1,12 @@
 import express from 'express';
 
 import { chatHandlersFactory } from './chat';
+import { rebuildVectorStoreHandlersFactory } from './rebuild-vector-store';
 
 const router = express.Router();
 
 module.exports = (crowi) => {
   router.post('/chat', chatHandlersFactory(crowi));
+  router.post('/rebuild-vector-store', rebuildVectorStoreHandlersFactory(crowi));
   return router;
 };

+ 30 - 0
apps/app/src/server/routes/apiv3/openai/rebuild-vector-store.ts

@@ -0,0 +1,30 @@
+import type { Request, RequestHandler } from 'express';
+import type { ValidationChain } from 'express-validator';
+
+import type Crowi from '~/server/crowi';
+import { certifyAiService } from '~/server/middlewares/certify-ai-service';
+import loggerFactory from '~/utils/logger';
+
+import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:openai:rebuild-vector-store');
+
+type RebuildVectorStoreFactory = (crowi: Crowi) => RequestHandler[];
+
+export const rebuildVectorStoreHandlersFactory: RebuildVectorStoreFactory = (crowi) => {
+  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+  const adminRequired = require('~/server/middlewares/admin-required')(crowi);
+
+  const validator: ValidationChain[] = [
+    //
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly, adminRequired, certifyAiService, validator, apiV3FormValidator,
+    async(req: Request, res: ApiV3Response) => {
+      return res.apiv3({});
+    },
+  ];
+};