|
@@ -2,7 +2,7 @@ import assert from 'node:assert';
|
|
|
import { Readable, Transform } from 'stream';
|
|
import { Readable, Transform } from 'stream';
|
|
|
import { pipeline } from 'stream/promises';
|
|
import { pipeline } from 'stream/promises';
|
|
|
|
|
|
|
|
-import type { Lang } from '@growi/core';
|
|
|
|
|
|
|
+import type { IUser, Ref, Lang } from '@growi/core';
|
|
|
import {
|
|
import {
|
|
|
PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
|
|
PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
|
|
|
} from '@growi/core';
|
|
} from '@growi/core';
|
|
@@ -61,7 +61,6 @@ const convertPathPatternsToRegExp = (pagePathPatterns: string[]): Array<string |
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-
|
|
|
|
|
export interface IOpenaiService {
|
|
export interface IOpenaiService {
|
|
|
getOrCreateThread(userId: string, vectorStoreRelation: VectorStoreDocument, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
|
|
getOrCreateThread(userId: string, vectorStoreRelation: VectorStoreDocument, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
|
|
|
getThreads(vectorStoreRelationId: string): Promise<ThreadRelationDocument[]>
|
|
getThreads(vectorStoreRelationId: string): Promise<ThreadRelationDocument[]>
|
|
@@ -74,13 +73,14 @@ export interface IOpenaiService {
|
|
|
getVectorStoreRelation(aiAssistantId: string): Promise<VectorStoreDocument>
|
|
getVectorStoreRelation(aiAssistantId: string): Promise<VectorStoreDocument>
|
|
|
getVectorStoreRelationsByPageIds(pageId: Types.ObjectId[]): Promise<VectorStoreDocument[]>;
|
|
getVectorStoreRelationsByPageIds(pageId: Types.ObjectId[]): Promise<VectorStoreDocument[]>;
|
|
|
createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
|
|
createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
|
|
|
|
|
+ createVectorStoreFileOnPageCreate(pages: PageDocument[]): Promise<void>;
|
|
|
|
|
+ updateVectorStoreFileOnPageUpdate(page: HydratedDocument<PageDocument>): Promise<void>;
|
|
|
deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId): Promise<void>;
|
|
deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId): Promise<void>;
|
|
|
deleteVectorStoreFilesByPageIds(pageIds: Types.ObjectId[]): Promise<void>;
|
|
deleteVectorStoreFilesByPageIds(pageIds: Types.ObjectId[]): Promise<void>;
|
|
|
deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
|
|
deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
|
|
|
// rebuildVectorStoreAll(): Promise<void>;
|
|
// rebuildVectorStoreAll(): Promise<void>;
|
|
|
// rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
|
|
// rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
|
|
|
isAiAssistantUsable(aiAssistantId: string, user: IUserHasId): Promise<boolean>;
|
|
isAiAssistantUsable(aiAssistantId: string, user: IUserHasId): Promise<boolean>;
|
|
|
- updateVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
|
|
|
|
|
createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
|
|
createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
|
|
|
updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
|
|
updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
|
|
|
getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
|
|
getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
|
|
@@ -325,7 +325,7 @@ class OpenaiService implements IOpenaiService {
|
|
|
// const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
|
|
// const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
|
|
|
const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
|
|
const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
|
|
|
const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
|
|
const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
|
|
|
- if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
|
|
|
|
|
|
|
+ if (page._id != null && page.revision != null) {
|
|
|
if (isPopulated(page.revision) && page.revision.body.length > 0) {
|
|
if (isPopulated(page.revision) && page.revision.body.length > 0) {
|
|
|
const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
|
|
const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
|
|
|
prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
|
|
prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
|
|
@@ -501,13 +501,92 @@ class OpenaiService implements IOpenaiService {
|
|
|
// await pipeline(pagesStream, batchStrem, createVectorStoreFileStream);
|
|
// await pipeline(pagesStream, batchStrem, createVectorStoreFileStream);
|
|
|
// }
|
|
// }
|
|
|
|
|
|
|
|
- async updateVectorStore(page: HydratedDocument<PageDocument>) {
|
|
|
|
|
- const vectorStoreRelations = await this.getVectorStoreRelationsByPageIds([page._id]);
|
|
|
|
|
- console.log('vectorStoreRelations', vectorStoreRelations);
|
|
|
|
|
- vectorStoreRelations.forEach(async(vectorStoreRelation) => {
|
|
|
|
|
- await this.deleteVectorStoreFile(vectorStoreRelation._id, page._id);
|
|
|
|
|
- await this.createVectorStoreFile(vectorStoreRelation, [page]);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ async filterPagesByAccessScope(aiAssistant: AiAssistantDocument, pages: HydratedDocument<PageDocument>[]) {
|
|
|
|
|
+ const isPublicPage = (page :HydratedDocument<PageDocument>) => page.grant === PageGrant.GRANT_PUBLIC;
|
|
|
|
|
+
|
|
|
|
|
+ const isUserGroupAccessible = (page :HydratedDocument<PageDocument>, ownerUserGroupIds: string[]) => {
|
|
|
|
|
+ if (page.grant !== PageGrant.GRANT_USER_GROUP) return false;
|
|
|
|
|
+ return page.grantedGroups.some(group => ownerUserGroupIds.includes(getIdStringForRef(group.item)));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const isOwnerAccessible = (page: HydratedDocument<PageDocument>, ownerId: Ref<IUser>) => {
|
|
|
|
|
+ if (page.grant !== PageGrant.GRANT_OWNER) return false;
|
|
|
|
|
+ return page.grantedUsers.some(user => getIdStringForRef(user) === getIdStringForRef(ownerId));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getOwnerUserGroupIds = async(owner: Ref<IUser>) => {
|
|
|
|
|
+ const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner);
|
|
|
|
|
+ const externalGroups = await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner);
|
|
|
|
|
+ return [...userGroups, ...externalGroups].map(group => getIdStringForRef(group));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ switch (aiAssistant.accessScope) {
|
|
|
|
|
+ case AiAssistantAccessScope.PUBLIC_ONLY:
|
|
|
|
|
+ return pages.filter(isPublicPage);
|
|
|
|
|
+
|
|
|
|
|
+ case AiAssistantAccessScope.GROUPS: {
|
|
|
|
|
+ const ownerUserGroupIds = await getOwnerUserGroupIds(aiAssistant.owner);
|
|
|
|
|
+ return pages.filter(page => isPublicPage(page) || isUserGroupAccessible(page, ownerUserGroupIds));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case AiAssistantAccessScope.OWNER: {
|
|
|
|
|
+ const ownerUserGroupIds = await getOwnerUserGroupIds(aiAssistant.owner);
|
|
|
|
|
+ return pages.filter(page => isPublicPage(page) || isOwnerAccessible(page, aiAssistant.owner) || isUserGroupAccessible(page, ownerUserGroupIds));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async createVectorStoreFileOnPageCreate(pages: HydratedDocument<PageDocument>[]): Promise<void> {
|
|
|
|
|
+ const pagePaths = pages.map(page => page.path);
|
|
|
|
|
+ const aiAssistants = await AiAssistantModel.findByPagePaths(pagePaths);
|
|
|
|
|
+
|
|
|
|
|
+ if (aiAssistants.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for await (const aiAssistant of aiAssistants) {
|
|
|
|
|
+ const pagesToVectorize = await this.filterPagesByAccessScope(aiAssistant, pages);
|
|
|
|
|
+ const vectorStoreRelation = aiAssistant.vectorStore;
|
|
|
|
|
+ if (vectorStoreRelation == null || !isPopulated(vectorStoreRelation)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.debug('--------- createVectorStoreFileOnPageCreate ---------');
|
|
|
|
|
+ logger.debug('AccessScopeType of aiAssistant: ', aiAssistant.accessScope);
|
|
|
|
|
+ logger.debug('VectorStoreFile pagePath to be created: ', pagesToVectorize.map(page => page.path));
|
|
|
|
|
+ logger.debug('-----------------------------------------------------');
|
|
|
|
|
+
|
|
|
|
|
+ await this.createVectorStoreFile(vectorStoreRelation as VectorStoreDocument, pagesToVectorize);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async updateVectorStoreFileOnPageUpdate(page: HydratedDocument<PageDocument>) {
|
|
|
|
|
+ const aiAssistants = await AiAssistantModel.findByPagePaths([page.path]);
|
|
|
|
|
+
|
|
|
|
|
+ if (aiAssistants.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for await (const aiAssistant of aiAssistants) {
|
|
|
|
|
+ const pagesToVectorize = await this.filterPagesByAccessScope(aiAssistant, [page]);
|
|
|
|
|
+ const vectorStoreRelation = aiAssistant.vectorStore;
|
|
|
|
|
+ if (vectorStoreRelation == null || !isPopulated(vectorStoreRelation)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.debug('---------- updateVectorStoreOnPageUpdate ------------');
|
|
|
|
|
+ logger.debug('AccessScopeType of aiAssistant: ', aiAssistant.accessScope);
|
|
|
|
|
+ logger.debug('PagePath of VectorStoreFile to be deleted: ', page.path);
|
|
|
|
|
+ logger.debug('pagePath of VectorStoreFile to be created: ', pagesToVectorize.map(page => page.path));
|
|
|
|
|
+ logger.debug('-----------------------------------------------------');
|
|
|
|
|
+
|
|
|
|
|
+ // Do not create a new VectorStoreFile if page is changed to a permission that AiAssistant does not have access to
|
|
|
|
|
+ await this.createVectorStoreFile(vectorStoreRelation as VectorStoreDocument, pagesToVectorize);
|
|
|
|
|
+ await this.deleteVectorStoreFile((vectorStoreRelation as VectorStoreDocument)._id, page._id);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async createVectorStoreFileWithStream(vectorStoreRelation: VectorStoreDocument, conditions: mongoose.FilterQuery<PageDocument>): Promise<void> {
|
|
private async createVectorStoreFileWithStream(vectorStoreRelation: VectorStoreDocument, conditions: mongoose.FilterQuery<PageDocument>): Promise<void> {
|