Explorar o código

Merge pull request #9545 from weseek/feat/160044-implement-logic-for-creating-pecialized-vector-store

feat: Implement logic for creating specialized VectorStore
Shun Miyazawa hai 1 ano
pai
achega
03377b8721

+ 2 - 2
apps/app/src/features/openai/interfaces/ai-assistant.ts

@@ -21,7 +21,7 @@ export const AiAssistantAccessScope = {
 *  Interfaces
 */
 export type AiAssistantShareScope = typeof AiAssistantShareScope[keyof typeof AiAssistantShareScope];
-export type AiAssistantOwnerAccessScope = typeof AiAssistantAccessScope[keyof typeof AiAssistantAccessScope];
+export type AiAssistantAccessScope = typeof AiAssistantAccessScope[keyof typeof AiAssistantAccessScope];
 
 export interface AiAssistant {
   name: string;
@@ -32,5 +32,5 @@ export interface AiAssistant {
   owner: Ref<IUser>
   grantedGroups?: IGrantedGroup[]
   shareScope: AiAssistantShareScope
-  ownerAccessScope: AiAssistantOwnerAccessScope
+  accessScope: AiAssistantAccessScope
 }

+ 1 - 1
apps/app/src/features/openai/server/models/ai-assistant.ts

@@ -70,7 +70,7 @@ const schema = new Schema<AiAssistantDocument>(
       enum: Object.values(AiAssistantShareScope),
       required: true,
     },
-    ownerAccessScope: {
+    accessScope: {
       type: String,
       enum: Object.values(AiAssistantAccessScope),
       required: true,

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

@@ -2,16 +2,8 @@ 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);
 export interface VectorStore {
   vectorStoreId: string
-  scopeType: VectorStoreScopeType
   isDeleted: boolean
 }
 
@@ -27,11 +19,6 @@ const schema = new Schema<VectorStoreDocument, VectorStoreModel>({
     required: true,
     unique: true,
   },
-  scopeType: {
-    enum: VectorStoreScopeTypes,
-    type: String,
-    required: true,
-  },
   isDeleted: {
     type: Boolean,
     default: false,

+ 13 - 3
apps/app/src/features/openai/server/routes/ai-assistant.ts

@@ -1,5 +1,6 @@
 import { type IUserHasId, GroupType } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { isGrobPatternPath, isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
 import type { Request, RequestHandler } from 'express';
 import { type ValidationChain, body } from 'express-validator';
 
@@ -60,7 +61,16 @@ export const createAiAssistantFactory: CreateAssistantFactory = (crowi) => {
       .isString()
       .withMessage('pagePathPatterns must be an array of strings')
       .notEmpty()
-      .withMessage('pagePathPatterns must not be empty'),
+      .withMessage('pagePathPatterns must not be empty')
+      .custom((value: string) => {
+
+        // check if the value is a grob pattern path
+        if (value.includes('*')) {
+          return isGrobPatternPath(value) && isCreatablePage(value.replace('*', ''));
+        }
+
+        return isCreatablePage(value);
+      }),
 
     body('grantedGroups')
       .optional()
@@ -79,9 +89,9 @@ export const createAiAssistantFactory: CreateAssistantFactory = (crowi) => {
       .isIn(Object.values(AiAssistantShareScope))
       .withMessage('Invalid shareScope value'),
 
-    body('ownerAccessScope')
+    body('accessScope')
       .isIn(Object.values(AiAssistantAccessScope))
-      .withMessage('Invalid ownerAccessScope value'),
+      .withMessage('Invalid accessScope value'),
   ];
 
   return [

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

@@ -32,7 +32,7 @@ export const factory = (crowi: Crowi): express.Router => {
     });
 
     import('./ai-assistant').then(({ createAiAssistantFactory }) => {
-      router.post('ai-assistant', createAiAssistantFactory(crowi));
+      router.post('/ai-assistant', createAiAssistantFactory(crowi));
     });
   }
 

+ 1 - 1
apps/app/src/features/openai/server/routes/rebuild-vector-store.ts

@@ -30,7 +30,7 @@ export const rebuildVectorStoreHandlersFactory: RebuildVectorStoreFactory = (cro
 
       try {
         const openaiService = getOpenaiService();
-        await openaiService?.rebuildVectorStoreAll();
+        // await openaiService?.rebuildVectorStoreAll();
         return res.apiv3({});
 
       }

+ 3 - 3
apps/app/src/features/openai/server/routes/thread.ts

@@ -33,9 +33,9 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
       try {
         const openaiService = getOpenaiService();
         const filterdThreadId = req.body.threadId != null ? filterXSS(req.body.threadId) : undefined;
-        const vectorStore = await openaiService?.getOrCreateVectorStoreForPublicScope();
-        const thread = await openaiService?.getOrCreateThread(req.user._id, vectorStore?.vectorStoreId, filterdThreadId);
-        return res.apiv3({ thread });
+        // const vectorStore = await openaiService?.getOrCreateVectorStoreForPublicScope();
+        // const thread = await openaiService?.getOrCreateThread(req.user._id, vectorStore?.vectorStoreId, filterdThreadId);
+        return res.apiv3({ });
       }
       catch (err) {
         logger.error(err);

+ 2 - 4
apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts

@@ -3,8 +3,6 @@ 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';
 
 
@@ -40,8 +38,8 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
     return this.client.beta.threads.del(threadId);
   }
 
-  async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
-    return this.client.beta.vectorStores.create({ name: `growi-vector-store-{${scopeType}` });
+  async createVectorStore(name: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.create({ name: `growi-vector-store-for-${name}` });
   }
 
   async retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {

+ 1 - 3
apps/app/src/features/openai/server/services/client-delegator/interfaces.ts

@@ -1,14 +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 {
   createThread(vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread>
   retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread>
   deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted>
   retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore>
-  createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore>
+  createVectorStore(name: string): Promise<OpenAI.Beta.VectorStores.VectorStore>
   deleteVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStoreDeleted>
   uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject>
   createVectorStoreFileBatch(vectorStoreId: string, fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch>

+ 2 - 3
apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts

@@ -1,7 +1,6 @@
 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';
@@ -42,8 +41,8 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
     return this.client.beta.threads.del(threadId);
   }
 
-  async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
-    return this.client.beta.vectorStores.create({ name: `growi-vector-store-${scopeType}` });
+  async createVectorStore(name: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {
+    return this.client.beta.vectorStores.create({ name: `growi-vector-store-for-${name}` });
   }
 
   async retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore> {

+ 139 - 68
apps/app/src/features/openai/server/services/openai.ts

@@ -3,13 +3,15 @@ import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 
 import { PageGrant, isPopulated } from '@growi/core';
+import { isGrobPatternPath } from '@growi/core/dist/utils/page-path-utils';
+import escapeStringRegexp from 'escape-string-regexp';
 import type { HydratedDocument, Types } from 'mongoose';
 import mongoose from 'mongoose';
 import type OpenAI from 'openai';
 import { toFile } from 'openai';
 
 import ThreadRelationModel from '~/features/openai/server/models/thread-relation';
-import VectorStoreModel, { VectorStoreScopeType, type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
+import VectorStoreModel, { type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
 import VectorStoreFileRelationModel, {
   type VectorStoreFileRelation,
   prepareVectorStoreFileRelations,
@@ -33,20 +35,34 @@ const BATCH_SIZE = 100;
 
 const logger = loggerFactory('growi:service:openai');
 
-let isVectorStoreForPublicScopeExist = false;
+// const isVectorStoreForPublicScopeExist = false;
 
 type VectorStoreFileRelationsMap = Map<string, VectorStoreFileRelation>
 
+
+const convertPathPatternsToRegExp = (pagePathPatterns: string[]): Array<string | RegExp> => {
+  return pagePathPatterns.map((pagePathPattern) => {
+    if (isGrobPatternPath(pagePathPattern)) {
+      const trimedPagePathPattern = pagePathPattern.replace('/*', '');
+      const escapedPagePathPattern = escapeStringRegexp(trimedPagePathPattern);
+      return new RegExp(`^${escapedPagePathPattern}`);
+    }
+
+    return pagePathPattern;
+  });
+};
+
+
 export interface IOpenaiService {
   getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
-  getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
+  // getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
   deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
-  createVectorStoreFile(pages: PageDocument[]): Promise<void>;
+  createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
   deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId): Promise<void>;
   deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
-  rebuildVectorStoreAll(): Promise<void>;
-  rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
+  // rebuildVectorStoreAll(): Promise<void>;
+  // rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
   createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
 }
 class OpenaiService implements IOpenaiService {
@@ -113,38 +129,55 @@ class OpenaiService implements IOpenaiService {
     await ThreadRelationModel.deleteMany({ threadId: { $in: deletedThreadIds } });
   }
 
-  public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
-    const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: VectorStoreScopeType.PUBLIC, isDeleted: false });
+  // TODO: https://redmine.weseek.co.jp/issues/160332
+  // public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
+  //   const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: VectorStoreScopeType.PUBLIC, isDeleted: false });
 
-    if (vectorStoreDocument != null && isVectorStoreForPublicScopeExist) {
-      return vectorStoreDocument;
-    }
+  //   if (vectorStoreDocument != null && isVectorStoreForPublicScopeExist) {
+  //     return vectorStoreDocument;
+  //   }
 
-    if (vectorStoreDocument != null && !isVectorStoreForPublicScopeExist) {
-      try {
-        // Check if vector store entity exists
-        // If the vector store entity does not exist, the vector store document is deleted
-        await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
-        isVectorStoreForPublicScopeExist = true;
-        return vectorStoreDocument;
-      }
-      catch (err) {
-        await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
-        throw new Error(err);
-      }
-    }
+  //   if (vectorStoreDocument != null && !isVectorStoreForPublicScopeExist) {
+  //     try {
+  //       // Check if vector store entity exists
+  //       // If the vector store entity does not exist, the vector store document is deleted
+  //       await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
+  //       isVectorStoreForPublicScopeExist = true;
+  //       return vectorStoreDocument;
+  //     }
+  //     catch (err) {
+  //       await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
+  //       throw new Error(err);
+  //     }
+  //   }
+
+  //   const newVectorStore = await this.client.createVectorStore(VectorStoreScopeType.PUBLIC);
+  //   const newVectorStoreDocument = await VectorStoreModel.create({
+  //     vectorStoreId: newVectorStore.id,
+  //     scopeType: VectorStoreScopeType.PUBLIC,
+  //   }) as VectorStoreDocument;
 
-    const newVectorStore = await this.client.createVectorStore(VectorStoreScopeType.PUBLIC);
-    const newVectorStoreDocument = await VectorStoreModel.create({
-      vectorStoreId: newVectorStore.id,
-      scopeType: VectorStoreScopeType.PUBLIC,
-    }) as VectorStoreDocument;
+  //   isVectorStoreForPublicScopeExist = true;
 
-    isVectorStoreForPublicScopeExist = true;
+  //   return newVectorStoreDocument;
+  // }
 
-    return newVectorStoreDocument;
+  private async createVectorStore(name: string): Promise<VectorStoreDocument> {
+    try {
+      const newVectorStore = await this.client.createVectorStore(name);
+
+      const newVectorStoreDocument = await VectorStoreModel.create({
+        vectorStoreId: newVectorStore.id,
+      }) as VectorStoreDocument;
+
+      return newVectorStoreDocument;
+    }
+    catch (err) {
+      throw new Error(err);
+    }
   }
 
+  // TODO: https://redmine.weseek.co.jp/issues/160332
   // TODO: https://redmine.weseek.co.jp/issues/156643
   // private async uploadFileByChunks(pageId: Types.ObjectId, body: string, vectorStoreFileRelationsMap: VectorStoreFileRelationsMap) {
   //   const chunks = await splitMarkdownIntoChunks(body, 'gpt-4o');
@@ -167,37 +200,38 @@ class OpenaiService implements IOpenaiService {
     return uploadedFile;
   }
 
-  private async deleteVectorStore(vectorStoreScopeType: VectorStoreScopeType): Promise<void> {
-    const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: vectorStoreScopeType, isDeleted: false });
-    if (vectorStoreDocument == null) {
-      return;
-    }
+  // 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;
+  //   }
 
-    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 oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
+  //     throw new Error(err);
+  //   }
+  // }
 
-  async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
-    const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
+  async createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
+    // const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
     const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
     const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
-          prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           return;
         }
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, page.path, pagePopulatedToShowRevision.revision.body);
-          prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
         }
       }
     };
@@ -228,7 +262,7 @@ class OpenaiService implements IOpenaiService {
       await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
 
       // Create vector store file
-      const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStore.vectorStoreId, uploadedFileIds);
+      const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStoreRelation.vectorStoreId, uploadedFileIds);
       logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
 
       // Set isAttachedToVectorStore: true when the uploaded file is attached to VectorStore
@@ -239,7 +273,7 @@ class OpenaiService implements IOpenaiService {
 
       // Delete all uploaded files if createVectorStoreFileBatch fails
       for await (const pageId of pageIds) {
-        await this.deleteVectorStoreFile(vectorStore._id, pageId);
+        await this.deleteVectorStoreFile(vectorStoreRelation._id, pageId);
       }
     }
 
@@ -331,36 +365,73 @@ class OpenaiService implements IOpenaiService {
     }
   }
 
-  async rebuildVectorStoreAll() {
-    await this.deleteVectorStore(VectorStoreScopeType.PUBLIC);
+  // TODO: https://redmine.weseek.co.jp/issues/160332
+  // async rebuildVectorStoreAll() {
+  //   await this.deleteVectorStore(VectorStoreScopeType.PUBLIC);
+
+  //   // Create all public pages VectorStoreFile
+  //   const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
+  //   const pagesStream = Page.find({ grant: PageGrant.GRANT_PUBLIC }).populate('revision').cursor({ batch_size: BATCH_SIZE });
+  //   const batchStrem = createBatchStream(BATCH_SIZE);
+
+  //   const createVectorStoreFile = this.createVectorStoreFile.bind(this);
+  //   const createVectorStoreFileStream = new Transform({
+  //     objectMode: true,
+  //     async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
+  //       await createVectorStoreFile(chunk);
+  //       this.push(chunk);
+  //       callback();
+  //     },
+  //   });
+
+  //   await pipeline(pagesStream, batchStrem, createVectorStoreFileStream);
+  // }
+
+  // async rebuildVectorStore(page: HydratedDocument<PageDocument>) {
+  //   const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
+  //   await this.deleteVectorStoreFile(vectorStore._id, page._id);
+  //   await this.createVectorStoreFile([page]);
+  // }
 
-    // Create all public pages VectorStoreFile
+  private async createVectorStoreFileWithStream(vectorStoreRelation: VectorStoreDocument, conditions: mongoose.FilterQuery<PageDocument>): Promise<void> {
     const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
-    const pagesStream = Page.find({ grant: PageGrant.GRANT_PUBLIC }).populate('revision').cursor({ batch_size: BATCH_SIZE });
-    const batchStrem = createBatchStream(BATCH_SIZE);
+
+    const pagesStream = Page.find({ ...conditions })
+      .populate('revision')
+      .cursor({ batchSize: BATCH_SIZE });
+    const batchStream = createBatchStream(BATCH_SIZE);
 
     const createVectorStoreFile = this.createVectorStoreFile.bind(this);
     const createVectorStoreFileStream = new Transform({
       objectMode: true,
       async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
-        await createVectorStoreFile(chunk);
-        this.push(chunk);
-        callback();
+        try {
+          await createVectorStoreFile(vectorStoreRelation, chunk);
+          this.push(chunk);
+          callback();
+        }
+        catch (error) {
+          callback(error);
+        }
       },
     });
 
-    await pipeline(pagesStream, batchStrem, createVectorStoreFileStream);
-  }
-
-  async rebuildVectorStore(page: HydratedDocument<PageDocument>) {
-    const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
-    await this.deleteVectorStoreFile(vectorStore._id, page._id);
-    await this.createVectorStoreFile([page]);
+    await pipeline(pagesStream, batchStream, createVectorStoreFileStream);
   }
 
   async createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument> {
-    const dumyVectorStoreId = '676e0d9863442b736e7ecf09';
-    const aiAssistant = await AiAssistantModel.create({ ...data, vectorStore: dumyVectorStoreId });
+    const vectorStoreRelation = await this.createVectorStore(data.name);
+    const aiAssistant = await AiAssistantModel.create({
+      ...data, vectorStore: vectorStoreRelation,
+    });
+
+    const conditions = {
+      path: { $in: convertPathPatternsToRegExp(data.pagePathPatterns) },
+    };
+
+    // VectorStore creation process does not await
+    this.createVectorStoreFileWithStream(vectorStoreRelation, conditions);
+
     return aiAssistant;
   }
 

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

@@ -205,8 +205,9 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     if (isAiEnabled()) {
       const { getOpenaiService } = await import('~/features/openai/server/services/openai');
       try {
+        // TODO: https://redmine.weseek.co.jp/issues/160334
         const openaiService = getOpenaiService();
-        await openaiService?.rebuildVectorStore(createdPage);
+        // await openaiService?.rebuildVectorStore(createdPage);
       }
       catch (err) {
         logger.error('Rebuild vector store failed', err);

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

@@ -121,8 +121,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     if (isAiEnabled()) {
       const { getOpenaiService } = await import('~/features/openai/server/services/openai');
       try {
+        // TODO: https://redmine.weseek.co.jp/issues/160335
         const openaiService = getOpenaiService();
-        await openaiService?.rebuildVectorStore(updatedPage);
+        // await openaiService?.rebuildVectorStore(updatedPage);
       }
       catch (err) {
         logger.error('Rebuild vector store failed', err);

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

@@ -1171,11 +1171,12 @@ class PageService implements IPageService {
       );
 
       if (isAiEnabled()) {
+        // TODO: https://redmine.weseek.co.jp/issues/160336
         const { getOpenaiService } = await import('~/features/openai/server/services/openai');
 
         // Do not await because communication with OpenAI takes time
         const openaiService = getOpenaiService();
-        openaiService?.createVectorStoreFile([duplicatedTarget]);
+        // openaiService?.createVectorStoreFile([duplicatedTarget]);
       }
     }
     this.pageEvent.emit('duplicate', page, user);
@@ -1412,11 +1413,12 @@ class PageService implements IPageService {
       .find({ _id: { $in: duplicatedPageIds }, grant: PageGrant.GRANT_PUBLIC }).populate('revision') as PageDocument[];
 
     if (isAiEnabled()) {
+      // TODO: https://redmine.weseek.co.jp/issues/160336
       const { getOpenaiService } = await import('~/features/openai/server/services/openai');
 
       // Do not await because communication with OpenAI takes time
       const openaiService = getOpenaiService();
-      openaiService?.createVectorStoreFile(duplicatedPagesWithPopulatedToShowRevison);
+      // openaiService?.createVectorStoreFile(duplicatedPagesWithPopulatedToShowRevison);
     }
   }
 
@@ -1898,11 +1900,12 @@ class PageService implements IPageService {
     if (isAiEnabled()) {
       const { getOpenaiService } = await import('~/features/openai/server/services/openai');
 
+      // TODO: https://redmine.weseek.co.jp/issues/160337
       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);
+        // const vectorStore = await openaiService.getOrCreateVectorStoreForPublicScope();
+        // const deleteVectorStoreFilePromises = pageIds.map(pageId => openaiService.deleteVectorStoreFile(vectorStore._id, pageId));
+        // await Promise.allSettled(deleteVectorStoreFilePromises);
       }
     }
   }

+ 6 - 0
packages/core/src/utils/page-path-utils/index.ts

@@ -305,5 +305,11 @@ export const getUsernameByPath = (path: string): string | null => {
   return username;
 };
 
+export const isGrobPatternPath = (path: string): boolean => {
+  // https://regex101.com/r/IBy7HS/1
+  const globPattern = /^(?:\/[^/*?[\]{}]+)*\/\*$/;
+  return globPattern.test(path);
+};
+
 
 export * from './is-top-page';