Răsfoiți Sursa

Merge branch 'feat/openai-vector-searching' into feat/153983-155025-split-and-merge-markdown-into-specified-tokens

nHigashiWeseek 1 an în urmă
părinte
comite
dcf83e21a0
33 a modificat fișierele cu 187 adăugiri și 132 ștergeri
  1. 1 1
      apps/app/src/client/components/PageControls/PageControls.tsx
  2. 2 1
      apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx
  3. 0 0
      apps/app/src/features/openai/client/components/AiIntegration/AiIntegration.tsx
  4. 0 0
      apps/app/src/features/openai/client/components/AiIntegration/AiIntegrationDisableMode.tsx
  5. 1 1
      apps/app/src/features/openai/client/components/RagSearchButton.module.scss
  6. 2 1
      apps/app/src/features/openai/client/components/RagSearchButton.tsx
  7. 0 0
      apps/app/src/features/openai/client/stores/rag-search.ts
  8. 0 0
      apps/app/src/features/openai/interfaces/ai.ts
  9. 34 0
      apps/app/src/features/openai/server/models/vector-store.ts
  10. 0 0
      apps/app/src/features/openai/server/routes/index.ts
  11. 8 7
      apps/app/src/features/openai/server/routes/message.ts
  12. 2 1
      apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts
  13. 5 4
      apps/app/src/features/openai/server/routes/rebuild-vector-store.ts
  14. 15 12
      apps/app/src/features/openai/server/routes/thread.ts
  15. 2 1
      apps/app/src/features/openai/server/services/assistant/assistant.ts
  16. 0 0
      apps/app/src/features/openai/server/services/assistant/index.ts
  17. 12 16
      apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts
  18. 1 1
      apps/app/src/features/openai/server/services/client-delegator/get-client.ts
  19. 0 0
      apps/app/src/features/openai/server/services/client-delegator/index.ts
  20. 12 0
      apps/app/src/features/openai/server/services/client-delegator/interfaces.ts
  21. 51 0
      apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts
  22. 0 0
      apps/app/src/features/openai/server/services/client.ts
  23. 0 0
      apps/app/src/features/openai/server/services/embeddings.ts
  24. 0 0
      apps/app/src/features/openai/server/services/index.ts
  25. 33 2
      apps/app/src/features/openai/server/services/openai.ts
  26. 2 2
      apps/app/src/pages/admin/ai-integration.page.tsx
  27. 1 1
      apps/app/src/server/routes/apiv3/index.js
  28. 1 1
      apps/app/src/server/routes/apiv3/page/create-page.ts
  29. 1 2
      apps/app/src/server/routes/apiv3/page/update-page.ts
  30. 0 6
      apps/app/src/server/service/config-loader.ts
  31. 0 12
      apps/app/src/server/service/openai/client-delegator/interfaces.ts
  32. 0 59
      apps/app/src/server/service/openai/client-delegator/openai-client-delegator.ts
  33. 1 1
      apps/app/src/server/service/page/index.ts

+ 1 - 1
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -16,6 +16,7 @@ import {
   toggleLike, toggleSubscribe,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
+import RagSearchButton from '~/features/openai/client/components/RagSearchButton';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSearchPage } from '~/stores-universal/context';
 import {
   EditorMode, useEditorMode,
@@ -36,7 +37,6 @@ import {
 
 import { BookmarkButtons } from './BookmarkButtons';
 import LikeButtons from './LikeButtons';
-import RagSearchButton from './RagSearchButton';
 import SearchButton from './SearchButton';
 import SeenUserInfo from './SeenUserInfo';
 import SubscribeButton from './SubscribeButton';

+ 2 - 1
apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx

@@ -8,9 +8,10 @@ import {
 } from 'reactstrap';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { useRagSearchModal } from '~/stores/rag-search';
 import loggerFactory from '~/utils/logger';
 
+import { useRagSearchModal } from '../../../client/stores/rag-search';
+
 import { MessageCard } from './MessageCard';
 import { ResizableTextarea } from './ResizableTextArea';
 

+ 0 - 0
apps/app/src/client/components/Admin/AiIntegration/AiIntegration.tsx → apps/app/src/features/openai/client/components/AiIntegration/AiIntegration.tsx


+ 0 - 0
apps/app/src/client/components/Admin/AiIntegration/AiIntegrationDisableMode.tsx → apps/app/src/features/openai/client/components/AiIntegration/AiIntegrationDisableMode.tsx


+ 1 - 1
apps/app/src/client/components/PageControls/RagSearchButton.module.scss → apps/app/src/features/openai/client/components/RagSearchButton.module.scss

@@ -1,7 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/core-styles/scss/variables/growi-official-colors';
 @use '@growi/ui/scss/atoms/btn-muted';
-@use './button-styles';
+@use '~/client/components/PageControls/button-styles';
 
 .btn-rag-search :global {
   @extend %btn-basis;

+ 2 - 1
apps/app/src/client/components/PageControls/RagSearchButton.tsx → apps/app/src/features/openai/client/components/RagSearchButton.tsx

@@ -2,7 +2,8 @@ import React, { useCallback } from 'react';
 
 import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
 import { useIsAiEnabled } from '~/stores-universal/context';
-import { useRagSearchModal } from '~/stores/rag-search';
+
+import { useRagSearchModal } from '../stores/rag-search';
 
 import styles from './RagSearchButton.module.scss';
 

+ 0 - 0
apps/app/src/stores/rag-search.ts → apps/app/src/features/openai/client/stores/rag-search.ts


+ 0 - 0
apps/app/src/interfaces/ai.ts → apps/app/src/features/openai/interfaces/ai.ts


+ 34 - 0
apps/app/src/features/openai/server/models/vector-store.ts

@@ -0,0 +1,34 @@
+import { type Model, type Document, Schema } from 'mongoose';
+
+import { getOrCreateModel } from '~/server/util/mongoose-utils';
+
+export const VectorStoreScopeType = {
+  PUBLIC: 'public',
+} as const;
+
+export type VectorStoreScopeType = typeof VectorStoreScopeType[keyof typeof VectorStoreScopeType];
+
+const VectorStoreScopeTypes = Object.values(VectorStoreScopeType);
+interface VectorStore {
+  vectorStoreId: string
+  scorpeType: VectorStoreScopeType
+}
+
+export interface VectorStoreDocument extends VectorStore, Document {}
+
+type VectorStoreModel = Model<VectorStore>
+
+const schema = new Schema<VectorStoreDocument, VectorStoreModel>({
+  vectorStoreId: {
+    type: String,
+    required: true,
+    unique: true,
+  },
+  scorpeType: {
+    enum: VectorStoreScopeTypes,
+    type: String,
+    required: true,
+  },
+});
+
+export default getOrCreateModel<VectorStoreDocument, VectorStoreModel>('VectorStore', schema);

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


+ 8 - 7
apps/app/src/server/routes/apiv3/openai/message.ts → apps/app/src/features/openai/server/routes/message.ts

@@ -6,15 +6,16 @@ import { body } from 'express-validator';
 import type { AssistantStream } from 'openai/lib/AssistantStream';
 import type { MessageDelta } from 'openai/resources/beta/threads/messages.mjs';
 
+import { getOrCreateChatAssistant } from '~/features/openai/server/services/assistant';
 import type Crowi from '~/server/crowi';
-import { openaiClient } from '~/server/service/openai';
-import { getOrCreateChatAssistant } from '~/server/service/openai/assistant';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import loggerFactory from '~/utils/logger';
 
-import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import { openaiClient } from '../services';
 
+import { certifyAiService } from './middlewares/certify-ai-service';
 
-const logger = loggerFactory('growi:routes:apiv3:openai:chat');
+const logger = loggerFactory('growi:routes:apiv3:openai:message');
 
 
 type ReqBody = {
@@ -27,8 +28,8 @@ type Req = Request<undefined, Response, ReqBody>
 type PostMessageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
 export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) => {
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [
     body('userMessage')
@@ -40,7 +41,7 @@ export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) =>
   ];
 
   return [
-    accessTokenParser, loginRequiredStrictly, validator, apiV3FormValidator,
+    accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: Response) => {
 
       const threadId = req.body.threadId;

+ 2 - 1
apps/app/src/server/middlewares/certify-ai-service.ts → apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts

@@ -1,9 +1,10 @@
 import type { NextFunction, Request, Response } from 'express';
 
-import { OpenaiServiceTypes } from '~/interfaces/ai';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
+import { OpenaiServiceTypes } from '../../../interfaces/ai';
+
 const logger = loggerFactory('growi:middlewares:certify-ai-service');
 
 export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {

+ 5 - 4
apps/app/src/server/routes/apiv3/openai/rebuild-vector-store.ts → apps/app/src/features/openai/server/routes/rebuild-vector-store.ts

@@ -3,12 +3,13 @@ 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 { getOpenaiService } from '~/server/service/openai/openai';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
-import type { ApiV3Response } from '../interfaces/apiv3-response';
+import { getOpenaiService } from '../services/openai';
+
+import { certifyAiService } from './middlewares/certify-ai-service';
 
 const logger = loggerFactory('growi:routes:apiv3:openai:rebuild-vector-store');
 

+ 15 - 12
apps/app/src/server/routes/apiv3/openai/thread.ts → apps/app/src/features/openai/server/routes/thread.ts

@@ -3,13 +3,16 @@ import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
 
 import type Crowi from '~/server/crowi';
-import { openaiClient } from '~/server/service/openai';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
-import type { ApiV3Response } from '../interfaces/apiv3-response';
+import { openaiClient } from '../services';
+import { getOpenaiService } from '../services/openai';
 
-const logger = loggerFactory('growi:routes:apiv3:openai:chat');
+import { certifyAiService } from './middlewares/certify-ai-service';
+
+const logger = loggerFactory('growi:routes:apiv3:openai:thread');
 
 type CreateThreadReq = Request<undefined, ApiV3Response, {
   userMessage: string,
@@ -19,29 +22,29 @@ type CreateThreadReq = Request<undefined, ApiV3Response, {
 type CreateThreadFactory = (crowi: Crowi) => RequestHandler[];
 
 export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [
     body('threadId').optional().isString().withMessage('threadId must be string'),
   ];
 
   return [
-    accessTokenParser, loginRequiredStrictly, validator, apiV3FormValidator,
+    accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: CreateThreadReq, res: ApiV3Response) => {
-
-      const vectorStoreId = process.env.OPENAI_VECTOR_STORE_ID;
-      if (vectorStoreId == null) {
-        return res.apiv3Err('OPENAI_VECTOR_STORE_ID is not setup', 503);
+      const openaiService = getOpenaiService();
+      if (openaiService == null) {
+        return res.apiv3Err('OpenaiService is not available', 503);
       }
 
       try {
+        const vectorStore = await openaiService.getOrCreateVectorStoreForPublicScope();
         const threadId = req.body.threadId;
         const thread = threadId == null
           ? await openaiClient.beta.threads.create({
             tool_resources: {
               file_search: {
-                vector_store_ids: [vectorStoreId],
+                vector_store_ids: [vectorStore.vectorStoreId],
               },
             },
           })

+ 2 - 1
apps/app/src/server/service/openai/assistant/assistant.ts → apps/app/src/features/openai/server/services/assistant/assistant.ts

@@ -1,6 +1,7 @@
 import type OpenAI from 'openai';
 
-import { configManager } from '../../config-manager';
+import { configManager } from '~/server/service/config-manager';
+
 import { openaiClient } from '../client';
 
 

+ 0 - 0
apps/app/src/server/service/openai/assistant/index.ts → apps/app/src/features/openai/server/services/assistant/index.ts


+ 12 - 16
apps/app/src/server/service/openai/client-delegator/azure-openai-client-delegator.ts → apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts

@@ -3,6 +3,8 @@ import type OpenAI from 'openai';
 import { AzureOpenAI } from 'openai';
 import { type Uploadable } from 'openai/uploads';
 
+import type { VectorStoreScopeType } from '~/features/openai/server/models/vector-store';
+
 import type { IOpenaiClientDelegator } from './interfaces';
 
 
@@ -10,8 +12,6 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
 
   private client: AzureOpenAI;
 
-  private openaiVectorStoreId: string;
-
   constructor() {
     // Retrieve Azure OpenAI related values from environment variables
     const credential = new DefaultAzureCredential();
@@ -22,32 +22,28 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
     // TODO: initialize openaiVectorStoreId property
   }
 
-  async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
-    return this.client.files.create({ file, purpose: 'assistants' });
-  }
-
-  async createVectorStoreFileBatch(fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
-    return this.client.beta.vectorStores.fileBatches.create(this.openaiVectorStoreId, { file_ids: fileIds });
+  async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.create({ name: `growi-vector-store-{${scopeType}` });
   }
 
-  async getFileList(): Promise<OpenAI.Files.FileObjectsPage> {
-    return this.client.files.list();
+  async retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.retrieve(vectorStoreId);
   }
 
-  async getVectorStoreFiles(): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFilesPage> {
-    return this.client.beta.vectorStores.files.list(this.openaiVectorStoreId);
+  async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
+    return this.client.files.create({ file, purpose: 'assistants' });
   }
 
-  async deleteVectorStoreFiles(fileId: string): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFileDeleted> {
-    return this.client.beta.vectorStores.files.del(this.openaiVectorStoreId, fileId);
+  async createVectorStoreFileBatch(vectorStoreId: string, fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
+    return this.client.beta.vectorStores.fileBatches.create(vectorStoreId, { file_ids: fileIds });
   }
 
   async deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted> {
     return this.client.files.del(fileId);
   }
 
-  async uploadAndPoll(files: Uploadable[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
-    return this.client.beta.vectorStores.fileBatches.uploadAndPoll(this.openaiVectorStoreId, { files });
+  async uploadAndPoll(vectorStoreId: string, files: Uploadable[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
+    return this.client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, { files });
   }
 
 }

+ 1 - 1
apps/app/src/server/service/openai/client-delegator/get-client.ts → apps/app/src/features/openai/server/services/client-delegator/get-client.ts

@@ -1,4 +1,4 @@
-import { OpenaiServiceType } from '~/interfaces/ai';
+import { OpenaiServiceType } from '../../../interfaces/ai';
 
 import { AzureOpenaiClientDelegator } from './azure-openai-client-delegator';
 import type { IOpenaiClientDelegator } from './interfaces';

+ 0 - 0
apps/app/src/server/service/openai/client-delegator/index.ts → apps/app/src/features/openai/server/services/client-delegator/index.ts


+ 12 - 0
apps/app/src/features/openai/server/services/client-delegator/interfaces.ts

@@ -0,0 +1,12 @@
+import type OpenAI from 'openai';
+import type { Uploadable } from 'openai/uploads';
+
+import type { VectorStoreScopeType } from '~/features/openai/server/models/vector-store';
+
+export interface IOpenaiClientDelegator {
+  retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore>
+  createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore>
+  uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject>
+  createVectorStoreFileBatch(vectorStoreId: string, fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch>
+  deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted>;
+}

+ 51 - 0
apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts

@@ -0,0 +1,51 @@
+import OpenAI from 'openai';
+import { type Uploadable } from 'openai/uploads';
+
+import type { VectorStoreScopeType } from '~/features/openai/server/models/vector-store';
+import { configManager } from '~/server/service/config-manager';
+
+import type { IOpenaiClientDelegator } from './interfaces';
+
+
+export class OpenaiClientDelegator implements IOpenaiClientDelegator {
+
+  private client: OpenAI;
+
+  constructor() {
+    // Retrieve OpenAI related values from environment variables
+    const apiKey = configManager.getConfig('crowi', 'openai:apiKey');
+
+    const isValid = [apiKey].every(value => value != null);
+    if (!isValid) {
+      throw new Error("Environment variables required to use OpenAI's API are not set");
+    }
+
+    // initialize client
+    this.client = new OpenAI({ apiKey });
+  }
+
+  async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.create({ name: `growi-vector-store-${scopeType}` });
+  }
+
+  async retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.retrieve(vectorStoreId);
+  }
+
+  async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
+    return this.client.files.create({ file, purpose: 'assistants' });
+  }
+
+  async createVectorStoreFileBatch(vectorStoreId: string, fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
+    return this.client.beta.vectorStores.fileBatches.create(vectorStoreId, { file_ids: fileIds });
+  }
+
+  async deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted> {
+    return this.client.files.del(fileId);
+  }
+
+  async uploadAndPoll(vectorStoreId: string, files: Uploadable[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
+    return this.client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, { files });
+  }
+
+}

+ 0 - 0
apps/app/src/server/service/openai/client.ts → apps/app/src/features/openai/server/services/client.ts


+ 0 - 0
apps/app/src/server/service/openai/embeddings.ts → apps/app/src/features/openai/server/services/embeddings.ts


+ 0 - 0
apps/app/src/server/service/openai/index.ts → apps/app/src/features/openai/server/services/index.ts


+ 33 - 2
apps/app/src/server/service/openai/openai.ts → apps/app/src/features/openai/server/services/openai.ts

@@ -7,16 +7,18 @@ import mongoose from 'mongoose';
 import type OpenAI from 'openai';
 import { toFile } from 'openai';
 
+import VectorStoreModel, { VectorStoreScopeType, type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
 import VectorStoreFileRelationModel, {
   type VectorStoreFileRelation,
   prepareVectorStoreFileRelations,
 } from '~/features/openai/server/models/vector-store-file-relation';
-import { OpenaiServiceTypes } from '~/interfaces/ai';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { configManager } from '~/server/service/config-manager';
 import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 
+import { OpenaiServiceTypes } from '../../interfaces/ai';
+
 
 import { getClient } from './client-delegator';
 
@@ -24,8 +26,10 @@ const BATCH_SIZE = 100;
 
 const logger = loggerFactory('growi:service:openai');
 
+let isVectorStoreForPublicScopeExist = false;
 
 export interface IOpenaiService {
+  getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
   createVectorStoreFile(pages: PageDocument[]): Promise<void>;
   deleteVectorStoreFile(pageId: Types.ObjectId): Promise<void>;
   rebuildVectorStoreAll(): Promise<void>;
@@ -38,6 +42,32 @@ class OpenaiService implements IOpenaiService {
     return getClient({ openaiServiceType });
   }
 
+  public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
+    const vectorStoreDocument = await VectorStoreModel.findOne({ scorpeType: VectorStoreScopeType.PUBLIC });
+
+    if (vectorStoreDocument != null && isVectorStoreForPublicScopeExist) {
+      return vectorStoreDocument;
+    }
+
+    if (vectorStoreDocument != null && !isVectorStoreForPublicScopeExist) {
+      const vectorStore = await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
+      if (vectorStore != null) {
+        isVectorStoreForPublicScopeExist = true;
+        return vectorStoreDocument;
+      }
+    }
+
+    const newVectorStore = await this.client.createVectorStore(VectorStoreScopeType.PUBLIC);
+    const newVectorStoreDocument = await VectorStoreModel.create({
+      vectorStoreId: newVectorStore.id,
+      scorpeType: VectorStoreScopeType.PUBLIC,
+    });
+
+    isVectorStoreForPublicScopeExist = true;
+
+    return newVectorStoreDocument;
+  }
+
   private async uploadFile(pageId: Types.ObjectId, body: string): Promise<OpenAI.Files.FileObject> {
     const file = await toFile(Readable.from(body), `${pageId}.md`);
     const uploadedFile = await this.client.uploadFile(file);
@@ -83,7 +113,8 @@ class OpenaiService implements IOpenaiService {
 
     try {
       // Create vector store file
-      const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(uploadedFileIds);
+      const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
+      const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStore.vectorStoreId, uploadedFileIds);
       logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
 
       // Save vector store file relation

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

@@ -13,9 +13,9 @@ 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 AiIntegration = dynamic(() => import('~/features/openai/client/components/AiIntegration/AiIntegration').then(mod => mod.AiIntegration), { ssr: false });
 const AiIntegrationDisableMode = dynamic(
-  () => import('~/client/components/Admin/AiIntegration/AiIntegrationDisableMode').then(mod => mod.AiIntegrationDisableMode), { ssr: false },
+  () => import('~/features/openai/client/components/AiIntegration/AiIntegrationDisableMode').then(mod => mod.AiIntegrationDisableMode), { ssr: false },
 );
 
 type Props = CommonProps & {

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

@@ -1,4 +1,5 @@
 import growiPlugin from '~/features/growi-plugin/server/routes/apiv3/admin';
+import openai from '~/features/openai/server/routes';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -8,7 +9,6 @@ import * as registerFormValidator from '../../middlewares/register-form-validato
 
 import g2gTransfer from './g2g-transfer';
 import importRoute from './import';
-import openai from './openai';
 import pageListing from './page-listing';
 import securitySettings from './security-settings';
 import * as userActivation from './user-activation';

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

@@ -11,6 +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 { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
@@ -23,7 +24,6 @@ import PageTagRelation from '~/server/models/page-tag-relation';
 import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import { configManager } from '~/server/service/config-manager';
 import { getTranslation } from '~/server/service/i18next';
-import { getOpenaiService } from '~/server/service/openai/openai';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';

+ 1 - 2
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -11,6 +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 { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
@@ -19,8 +20,6 @@ import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
-import { configManager } from '~/server/service/config-manager';
-import { getOpenaiService } from '~/server/service/openai/openai';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
 import { getYjsService } from '~/server/service/yjs';

+ 0 - 6
apps/app/src/server/service/config-loader.ts

@@ -806,12 +806,6 @@ const ENV_VAR_NAME_TO_CONFIG_INFO: Record<string, EnvConfig> = {
     type: ValueType.STRING,
     default: null,
   },
-  OPENAI_VECTOR_STORE_ID: {
-    ns: 'crowi',
-    key: 'openai:vectorStoreId',
-    type: ValueType.STRING,
-    default: null,
-  },
 };
 
 

+ 0 - 12
apps/app/src/server/service/openai/client-delegator/interfaces.ts

@@ -1,12 +0,0 @@
-import type OpenAI from 'openai';
-import type { Uploadable } from 'openai/uploads';
-
-export interface IOpenaiClientDelegator {
-  uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject>
-  createVectorStoreFileBatch(fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch>
-  getVectorStoreFiles(): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFilesPage>;
-  deleteVectorStoreFiles(fileId: string): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFileDeleted>;
-  getFileList(): Promise<OpenAI.Files.FileObjectsPage>;
-  deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted>;
-  uploadAndPoll(files: Uploadable[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch>;
-}

+ 0 - 59
apps/app/src/server/service/openai/client-delegator/openai-client-delegator.ts

@@ -1,59 +0,0 @@
-import OpenAI from 'openai';
-import { type Uploadable } from 'openai/uploads';
-
-import { configManager } from '~/server/service/config-manager';
-
-import type { IOpenaiClientDelegator } from './interfaces';
-
-
-export class OpenaiClientDelegator implements IOpenaiClientDelegator {
-
-  private client: OpenAI;
-
-  private openaiVectorStoreId: string;
-
-  constructor() {
-    // Retrieve OpenAI related values from environment variables
-    const apiKey = configManager.getConfig('crowi', 'openai:apiKey');
-    const vectorStoreId = configManager.getConfig('crowi', 'openai:vectorStoreId');
-
-    const isValid = [apiKey, vectorStoreId].every(value => value != null);
-    if (!isValid) {
-      throw new Error("Environment variables required to use OpenAI's API are not set");
-    }
-
-    this.openaiVectorStoreId = vectorStoreId;
-
-    // initialize client
-    this.client = new OpenAI({ apiKey });
-  }
-
-  async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
-    return this.client.files.create({ file, purpose: 'assistants' });
-  }
-
-  async createVectorStoreFileBatch(fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
-    return this.client.beta.vectorStores.fileBatches.create(this.openaiVectorStoreId, { file_ids: fileIds });
-  }
-
-  async getVectorStoreFiles(): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFilesPage> {
-    return this.client.beta.vectorStores.files.list(this.openaiVectorStoreId);
-  }
-
-  async deleteVectorStoreFiles(fileId: string): Promise<OpenAI.Beta.VectorStores.Files.VectorStoreFileDeleted> {
-    return this.client.beta.vectorStores.files.del(this.openaiVectorStoreId, fileId);
-  }
-
-  async getFileList(): Promise<OpenAI.Files.FileObjectsPage> {
-    return this.client.files.list();
-  }
-
-  async deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted> {
-    return this.client.files.del(fileId);
-  }
-
-  async uploadAndPoll(files: Uploadable[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch> {
-    return this.client.beta.vectorStores.fileBatches.uploadAndPoll(this.openaiVectorStoreId, { files });
-  }
-
-}

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

@@ -22,6 +22,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 { SupportedAction } from '~/interfaces/activity';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
@@ -43,7 +44,6 @@ import {
 import type { PageTagRelationDocument } from '~/server/models/page-tag-relation';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import type { UserGroupDocument } from '~/server/models/user-group';
-import { getOpenaiService } from '~/server/service/openai/openai';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { collectAncestorPaths } from '~/server/util/collect-ancestor-paths';
 import { generalXssFilter } from '~/services/general-xss-filter';