|
|
@@ -2,13 +2,13 @@ import assert from 'node:assert';
|
|
|
import { Readable, Transform } from 'stream';
|
|
|
import { pipeline } from 'stream/promises';
|
|
|
|
|
|
-import { PageGrant, getIdForRef, isPopulated } from '@growi/core';
|
|
|
+import {
|
|
|
+ PageGrant, getIdForRef, isPopulated, type IUserHasId,
|
|
|
+} 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 mongoose, { type HydratedDocument, type Types } from 'mongoose';
|
|
|
+import { type OpenAI, toFile } from 'openai';
|
|
|
|
|
|
import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
|
|
|
import ThreadRelationModel from '~/features/openai/server/models/thread-relation';
|
|
|
@@ -24,7 +24,9 @@ import { createBatchStream } from '~/server/util/batch-stream';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
import { OpenaiServiceTypes } from '../../interfaces/ai';
|
|
|
-import { type AiAssistant, AiAssistantAccessScope } from '../../interfaces/ai-assistant';
|
|
|
+import {
|
|
|
+ type AccessibleAiAssistants, type AiAssistant, AiAssistantAccessScope, AiAssistantShareScope,
|
|
|
+} from '../../interfaces/ai-assistant';
|
|
|
import AiAssistantModel, { type AiAssistantDocument } from '../models/ai-assistant';
|
|
|
import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
|
|
|
|
|
|
@@ -66,6 +68,7 @@ export interface IOpenaiService {
|
|
|
// rebuildVectorStoreAll(): Promise<void>;
|
|
|
// rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
|
|
|
createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
|
|
|
+ getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
|
|
|
}
|
|
|
class OpenaiService implements IOpenaiService {
|
|
|
|
|
|
@@ -425,10 +428,11 @@ class OpenaiService implements IOpenaiService {
|
|
|
private async createConditionForCreateAiAssistant(
|
|
|
owner: AiAssistant['owner'],
|
|
|
accessScope: AiAssistant['accessScope'],
|
|
|
- grantedGroups: AiAssistant['grantedGroups'],
|
|
|
+ grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
|
|
|
pagePathPatterns: AiAssistant['pagePathPatterns'],
|
|
|
): Promise<mongoose.FilterQuery<PageDocument>> {
|
|
|
- const converterdPagePatgPatterns = convertPathPatternsToRegExp(pagePathPatterns);
|
|
|
+
|
|
|
+ const converterdPagePathPatterns = convertPathPatternsToRegExp(pagePathPatterns);
|
|
|
|
|
|
// Include pages in search targets when their paths with 'Anyone with the link' permission are directly specified instead of using glob pattern
|
|
|
const nonGrabPagePathPatterns = pagePathPatterns.filter(pagePathPattern => !isGrobPatternPath(pagePathPattern));
|
|
|
@@ -443,37 +447,27 @@ class OpenaiService implements IOpenaiService {
|
|
|
baseCondition,
|
|
|
{
|
|
|
grant: PageGrant.GRANT_PUBLIC,
|
|
|
- path: { $in: converterdPagePatgPatterns },
|
|
|
+ path: { $in: converterdPagePathPatterns },
|
|
|
},
|
|
|
],
|
|
|
};
|
|
|
}
|
|
|
|
|
|
if (accessScope === AiAssistantAccessScope.GROUPS) {
|
|
|
- if (grantedGroups == null || grantedGroups.length === 0) {
|
|
|
+ if (grantedGroupsForAccessScope == null || grantedGroupsForAccessScope.length === 0) {
|
|
|
throw new Error('grantedGroups is required when accessScope is GROUPS');
|
|
|
}
|
|
|
|
|
|
- const extractedGrantedGroupIds = grantedGroups.map(group => getIdForRef(group.item).toString());
|
|
|
- const extractedOwnerGroupIds = [
|
|
|
- ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
- ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
- ].map(group => group.toString());
|
|
|
-
|
|
|
- // Check if the owner belongs to the group specified in grantedGroups
|
|
|
- const isValid = extractedGrantedGroupIds.every(groupId => extractedOwnerGroupIds.includes(groupId));
|
|
|
- if (!isValid) {
|
|
|
- throw new Error('A group to which the owner does not belong is specified.');
|
|
|
- }
|
|
|
+ const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
|
|
|
|
|
|
return {
|
|
|
$or: [
|
|
|
baseCondition,
|
|
|
{
|
|
|
grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP] },
|
|
|
- path: { $in: converterdPagePatgPatterns },
|
|
|
+ path: { $in: converterdPagePathPatterns },
|
|
|
$or: [
|
|
|
- { 'grantedGroups.item': { $in: extractedGrantedGroupIds } },
|
|
|
+ { 'grantedGroups.item': { $in: extractedGrantedGroupIdsForAccessScope } },
|
|
|
{ grant: PageGrant.GRANT_PUBLIC },
|
|
|
],
|
|
|
},
|
|
|
@@ -482,7 +476,7 @@ class OpenaiService implements IOpenaiService {
|
|
|
}
|
|
|
|
|
|
if (accessScope === AiAssistantAccessScope.OWNER) {
|
|
|
- const ownerUserGroup = [
|
|
|
+ const ownerUserGroups = [
|
|
|
...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
].map(group => group.toString());
|
|
|
@@ -492,9 +486,9 @@ class OpenaiService implements IOpenaiService {
|
|
|
baseCondition,
|
|
|
{
|
|
|
grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP, PageGrant.GRANT_OWNER] },
|
|
|
- path: { $in: converterdPagePatgPatterns },
|
|
|
+ path: { $in: converterdPagePathPatterns },
|
|
|
$or: [
|
|
|
- { 'grantedGroups.item': { $in: ownerUserGroup } },
|
|
|
+ { 'grantedGroups.item': { $in: ownerUserGroups } },
|
|
|
{ grantedUsers: { $in: [getIdForRef(owner)] } },
|
|
|
{ grant: PageGrant.GRANT_PUBLIC },
|
|
|
],
|
|
|
@@ -506,8 +500,63 @@ class OpenaiService implements IOpenaiService {
|
|
|
throw new Error('Invalid accessScope value');
|
|
|
}
|
|
|
|
|
|
+ private async validateGrantedUserGroupsForCreateAiAssistant(
|
|
|
+ owner: AiAssistant['owner'],
|
|
|
+ shareScope: AiAssistant['shareScope'],
|
|
|
+ accessScope: AiAssistant['accessScope'],
|
|
|
+ grantedGroupsForShareScope: AiAssistant['grantedGroupsForShareScope'],
|
|
|
+ grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
|
|
|
+ ) {
|
|
|
+
|
|
|
+ // Check if grantedGroupsForShareScope is not specified when shareScope is not a “group”
|
|
|
+ if (shareScope !== AiAssistantShareScope.GROUPS && grantedGroupsForShareScope != null) {
|
|
|
+ throw new Error('grantedGroupsForShareScope is specified when shareScope is not “groups”.');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if grantedGroupsForAccessScope is not specified when accessScope is not a “group”
|
|
|
+ if (accessScope !== AiAssistantAccessScope.GROUPS && grantedGroupsForAccessScope != null) {
|
|
|
+ throw new Error('grantedGroupsForAccessScope is specified when accsessScope is not “groups”.');
|
|
|
+ }
|
|
|
+
|
|
|
+ const ownerUserGroupIds = [
|
|
|
+ ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
+ ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
|
|
|
+ ].map(group => group.toString());
|
|
|
+
|
|
|
+ // Check if the owner belongs to the group specified in grantedGroupsForShareScope
|
|
|
+ if (grantedGroupsForShareScope != null && grantedGroupsForShareScope.length > 0) {
|
|
|
+ const extractedGrantedGroupIdsForShareScope = grantedGroupsForShareScope.map(group => getIdForRef(group.item).toString());
|
|
|
+ const isValid = extractedGrantedGroupIdsForShareScope.every(groupId => ownerUserGroupIds.includes(groupId));
|
|
|
+ if (!isValid) {
|
|
|
+ throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForShareScope');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if the owner belongs to the group specified in grantedGroupsForAccessScope
|
|
|
+ if (grantedGroupsForAccessScope != null && grantedGroupsForAccessScope.length > 0) {
|
|
|
+ const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
|
|
|
+ const isValid = extractedGrantedGroupIdsForAccessScope.every(groupId => ownerUserGroupIds.includes(groupId));
|
|
|
+ if (!isValid) {
|
|
|
+ throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForAccessScope');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument> {
|
|
|
- const conditions = await this.createConditionForCreateAiAssistant(data.owner, data.accessScope, data.grantedGroups, data.pagePathPatterns);
|
|
|
+ await this.validateGrantedUserGroupsForCreateAiAssistant(
|
|
|
+ data.owner,
|
|
|
+ data.shareScope,
|
|
|
+ data.accessScope,
|
|
|
+ data.grantedGroupsForShareScope,
|
|
|
+ data.grantedGroupsForAccessScope,
|
|
|
+ );
|
|
|
+
|
|
|
+ const conditions = await this.createConditionForCreateAiAssistant(
|
|
|
+ data.owner,
|
|
|
+ data.accessScope,
|
|
|
+ data.grantedGroupsForAccessScope,
|
|
|
+ data.pagePathPatterns,
|
|
|
+ );
|
|
|
|
|
|
const vectorStoreRelation = await this.createVectorStore(data.name);
|
|
|
const aiAssistant = await AiAssistantModel.create({
|
|
|
@@ -520,6 +569,42 @@ class OpenaiService implements IOpenaiService {
|
|
|
return aiAssistant;
|
|
|
}
|
|
|
|
|
|
+ async getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants> {
|
|
|
+ const userGroupIds = [
|
|
|
+ ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
|
|
|
+ ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
|
|
|
+ ];
|
|
|
+
|
|
|
+ const assistants = await AiAssistantModel.find({
|
|
|
+ $or: [
|
|
|
+ // Case 1: Assistants owned by the user
|
|
|
+ { owner: user },
|
|
|
+
|
|
|
+ // Case 2: Public assistants owned by others
|
|
|
+ {
|
|
|
+ $and: [
|
|
|
+ { owner: { $ne: user } },
|
|
|
+ { shareScope: AiAssistantShareScope.PUBLIC_ONLY },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+
|
|
|
+ // Case 3: Group-restricted assistants where user is in granted groups
|
|
|
+ {
|
|
|
+ $and: [
|
|
|
+ { owner: { $ne: user } },
|
|
|
+ { shareScope: AiAssistantShareScope.GROUPS },
|
|
|
+ { 'grantedGroupsForShareScope.item': { $in: userGroupIds } },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ myAiAssistants: assistants.filter(assistant => assistant.owner.toString() === user._id.toString()) ?? [],
|
|
|
+ teamAiAssistants: assistants.filter(assistant => assistant.owner.toString() !== user._id.toString()) ?? [],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
let instance: OpenaiService;
|