openai.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. import assert from 'node:assert';
  2. import { Readable, Transform } from 'stream';
  3. import { pipeline } from 'stream/promises';
  4. import {
  5. PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
  6. } from '@growi/core';
  7. import { deepEquals } from '@growi/core/dist/utils';
  8. import { isGrobPatternPath } from '@growi/core/dist/utils/page-path-utils';
  9. import escapeStringRegexp from 'escape-string-regexp';
  10. import createError from 'http-errors';
  11. import mongoose, { type HydratedDocument, type Types } from 'mongoose';
  12. import { type OpenAI, toFile } from 'openai';
  13. import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
  14. import ThreadRelationModel from '~/features/openai/server/models/thread-relation';
  15. import VectorStoreModel, { type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
  16. import VectorStoreFileRelationModel, {
  17. type VectorStoreFileRelation,
  18. prepareVectorStoreFileRelations,
  19. } from '~/features/openai/server/models/vector-store-file-relation';
  20. import type { PageDocument, PageModel } from '~/server/models/page';
  21. import UserGroupRelation from '~/server/models/user-group-relation';
  22. import { configManager } from '~/server/service/config-manager';
  23. import { createBatchStream } from '~/server/util/batch-stream';
  24. import loggerFactory from '~/utils/logger';
  25. import { OpenaiServiceTypes } from '../../interfaces/ai';
  26. import {
  27. type AccessibleAiAssistants, type AiAssistant, AiAssistantAccessScope, AiAssistantShareScope,
  28. } from '../../interfaces/ai-assistant';
  29. import AiAssistantModel, { type AiAssistantDocument } from '../models/ai-assistant';
  30. import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
  31. import { getClient } from './client-delegator';
  32. // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
  33. import { openaiApiErrorHandler } from './openai-api-error-handler';
  34. const { isDeepEquals } = deepEquals;
  35. const BATCH_SIZE = 100;
  36. const logger = loggerFactory('growi:service:openai');
  37. // const isVectorStoreForPublicScopeExist = false;
  38. type VectorStoreFileRelationsMap = Map<string, VectorStoreFileRelation>
  39. const convertPathPatternsToRegExp = (pagePathPatterns: string[]): Array<string | RegExp> => {
  40. return pagePathPatterns.map((pagePathPattern) => {
  41. if (isGrobPatternPath(pagePathPattern)) {
  42. const trimedPagePathPattern = pagePathPattern.replace('/*', '');
  43. const escapedPagePathPattern = escapeStringRegexp(trimedPagePathPattern);
  44. return new RegExp(`^${escapedPagePathPattern}`);
  45. }
  46. return pagePathPattern;
  47. });
  48. };
  49. export interface IOpenaiService {
  50. getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
  51. // getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
  52. deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
  53. deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
  54. getVectorStoreRelationsByPageIds(pageId: Types.ObjectId[]): Promise<VectorStoreDocument[]>;
  55. createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
  56. deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId): Promise<void>;
  57. deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
  58. // rebuildVectorStoreAll(): Promise<void>;
  59. updateVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
  60. createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
  61. updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
  62. getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
  63. deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument>
  64. }
  65. class OpenaiService implements IOpenaiService {
  66. private get client() {
  67. const openaiServiceType = configManager.getConfig('openai:serviceType');
  68. return getClient({ openaiServiceType });
  69. }
  70. public async getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread> {
  71. if (vectorStoreId != null && threadId == null) {
  72. try {
  73. const thread = await this.client.createThread(vectorStoreId);
  74. await ThreadRelationModel.create({ userId, threadId: thread.id });
  75. return thread;
  76. }
  77. catch (err) {
  78. throw new Error(err);
  79. }
  80. }
  81. const threadRelation = await ThreadRelationModel.findOne({ threadId });
  82. if (threadRelation == null) {
  83. throw new Error('ThreadRelation document is not exists');
  84. }
  85. // Check if a thread entity exists
  86. // If the thread entity does not exist, the thread-relation document is deleted
  87. try {
  88. const thread = await this.client.retrieveThread(threadRelation.threadId);
  89. // Update expiration date if thread entity exists
  90. await threadRelation.updateThreadExpiration();
  91. return thread;
  92. }
  93. catch (err) {
  94. await openaiApiErrorHandler(err, { notFoundError: async() => { await threadRelation.remove() } });
  95. throw new Error(err);
  96. }
  97. }
  98. public async deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void> {
  99. const expiredThreadRelations = await ThreadRelationModel.getExpiredThreadRelations(limit);
  100. if (expiredThreadRelations == null) {
  101. return;
  102. }
  103. const deletedThreadIds: string[] = [];
  104. for await (const expiredThreadRelation of expiredThreadRelations) {
  105. try {
  106. const deleteThreadResponse = await this.client.deleteThread(expiredThreadRelation.threadId);
  107. logger.debug('Delete thread', deleteThreadResponse);
  108. deletedThreadIds.push(expiredThreadRelation.threadId);
  109. // sleep
  110. await new Promise(resolve => setTimeout(resolve, apiCallInterval));
  111. }
  112. catch (err) {
  113. logger.error(err);
  114. }
  115. }
  116. await ThreadRelationModel.deleteMany({ threadId: { $in: deletedThreadIds } });
  117. }
  118. // TODO: https://redmine.weseek.co.jp/issues/160332
  119. // public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
  120. // const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: VectorStoreScopeType.PUBLIC, isDeleted: false });
  121. // if (vectorStoreDocument != null && isVectorStoreForPublicScopeExist) {
  122. // return vectorStoreDocument;
  123. // }
  124. // if (vectorStoreDocument != null && !isVectorStoreForPublicScopeExist) {
  125. // try {
  126. // // Check if vector store entity exists
  127. // // If the vector store entity does not exist, the vector store document is deleted
  128. // await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
  129. // isVectorStoreForPublicScopeExist = true;
  130. // return vectorStoreDocument;
  131. // }
  132. // catch (err) {
  133. // await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
  134. // throw new Error(err);
  135. // }
  136. // }
  137. // const newVectorStore = await this.client.createVectorStore(VectorStoreScopeType.PUBLIC);
  138. // const newVectorStoreDocument = await VectorStoreModel.create({
  139. // vectorStoreId: newVectorStore.id,
  140. // scopeType: VectorStoreScopeType.PUBLIC,
  141. // }) as VectorStoreDocument;
  142. // isVectorStoreForPublicScopeExist = true;
  143. // return newVectorStoreDocument;
  144. // }
  145. async getVectorStoreRelationsByPageIds(pageIds: Types.ObjectId[]): Promise<VectorStoreDocument[]> {
  146. const pipeline = [
  147. // Stage 1: Match documents with the given pageId
  148. {
  149. $match: {
  150. page: {
  151. $in: pageIds,
  152. },
  153. },
  154. },
  155. // Stage 2: Lookup VectorStore documents
  156. {
  157. $lookup: {
  158. from: 'vectorstores',
  159. localField: 'vectorStoreRelationId',
  160. foreignField: '_id',
  161. as: 'vectorStore',
  162. },
  163. },
  164. // Stage 3: Unwind the vectorStore array
  165. {
  166. $unwind: '$vectorStore',
  167. },
  168. // Stage 4: Match non-deleted vector stores
  169. {
  170. $match: {
  171. 'vectorStore.isDeleted': false,
  172. },
  173. },
  174. // Stage 5: Replace the root with vectorStore document
  175. {
  176. $replaceRoot: {
  177. newRoot: '$vectorStore',
  178. },
  179. },
  180. ];
  181. const vectorStoreRelations = await VectorStoreFileRelationModel.aggregate<VectorStoreDocument>(pipeline);
  182. return vectorStoreRelations;
  183. }
  184. private async createVectorStore(name: string): Promise<VectorStoreDocument> {
  185. try {
  186. const newVectorStore = await this.client.createVectorStore(name);
  187. const newVectorStoreDocument = await VectorStoreModel.create({
  188. vectorStoreId: newVectorStore.id,
  189. }) as VectorStoreDocument;
  190. return newVectorStoreDocument;
  191. }
  192. catch (err) {
  193. throw new Error(err);
  194. }
  195. }
  196. // TODO: https://redmine.weseek.co.jp/issues/160332
  197. // TODO: https://redmine.weseek.co.jp/issues/156643
  198. // private async uploadFileByChunks(pageId: Types.ObjectId, body: string, vectorStoreFileRelationsMap: VectorStoreFileRelationsMap) {
  199. // const chunks = await splitMarkdownIntoChunks(body, 'gpt-4o');
  200. // for await (const [index, chunk] of chunks.entries()) {
  201. // try {
  202. // const file = await toFile(Readable.from(chunk), `${pageId}-chunk-${index}.md`);
  203. // const uploadedFile = await this.client.uploadFile(file);
  204. // prepareVectorStoreFileRelations(pageId, uploadedFile.id, vectorStoreFileRelationsMap);
  205. // }
  206. // catch (err) {
  207. // logger.error(err);
  208. // }
  209. // }
  210. // }
  211. private async uploadFile(pageId: Types.ObjectId, pagePath: string, revisionBody: string): Promise<OpenAI.Files.FileObject> {
  212. const convertedHtml = await convertMarkdownToHtml({ pagePath, revisionBody });
  213. const file = await toFile(Readable.from(convertedHtml), `${pageId}.html`);
  214. const uploadedFile = await this.client.uploadFile(file);
  215. return uploadedFile;
  216. }
  217. private async deleteVectorStore(vectorStoreRelationId: string): Promise<void> {
  218. const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ _id: vectorStoreRelationId, isDeleted: false });
  219. if (vectorStoreDocument == null) {
  220. return;
  221. }
  222. try {
  223. await this.client.deleteVectorStore(vectorStoreDocument.vectorStoreId);
  224. await vectorStoreDocument.markAsDeleted();
  225. }
  226. catch (err) {
  227. await openaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
  228. throw new Error(err);
  229. }
  230. }
  231. async createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
  232. // const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
  233. const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
  234. const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
  235. if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
  236. if (isPopulated(page.revision) && page.revision.body.length > 0) {
  237. const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
  238. prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
  239. return;
  240. }
  241. const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
  242. if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
  243. const uploadedFile = await this.uploadFile(page._id, page.path, pagePopulatedToShowRevision.revision.body);
  244. prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
  245. }
  246. }
  247. };
  248. // Start workers to process results
  249. const workers = pages.map(processUploadFile);
  250. // Wait for all processing to complete.
  251. assert(workers.length <= BATCH_SIZE, 'workers.length must be less than or equal to BATCH_SIZE');
  252. const fileUploadResult = await Promise.allSettled(workers);
  253. fileUploadResult.forEach((result) => {
  254. if (result.status === 'rejected') {
  255. logger.error(result.reason);
  256. }
  257. });
  258. const vectorStoreFileRelations = Array.from(vectorStoreFileRelationsMap.values());
  259. const uploadedFileIds = vectorStoreFileRelations.map(data => data.fileIds).flat();
  260. if (uploadedFileIds.length === 0) {
  261. return;
  262. }
  263. const pageIds = pages.map(page => page._id);
  264. try {
  265. // Save vector store file relation
  266. await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
  267. // Create vector store file
  268. const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStoreRelation.vectorStoreId, uploadedFileIds);
  269. logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
  270. // Set isAttachedToVectorStore: true when the uploaded file is attached to VectorStore
  271. await VectorStoreFileRelationModel.markAsAttachedToVectorStore(pageIds);
  272. }
  273. catch (err) {
  274. logger.error(err);
  275. // Delete all uploaded files if createVectorStoreFileBatch fails
  276. for await (const pageId of pageIds) {
  277. await this.deleteVectorStoreFile(vectorStoreRelation._id, pageId);
  278. }
  279. }
  280. }
  281. // Deletes all VectorStore documents that are marked as deleted (isDeleted: true) and have no associated VectorStoreFileRelation documents
  282. async deleteObsolatedVectorStoreRelations(): Promise<void> {
  283. const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
  284. if (deletedVectorStoreRelations.length === 0) {
  285. return;
  286. }
  287. const currentVectorStoreRelationIds: Types.ObjectId[] = await VectorStoreFileRelationModel.aggregate([
  288. {
  289. $group: {
  290. _id: '$vectorStoreRelationId',
  291. relationCount: { $sum: 1 },
  292. },
  293. },
  294. { $match: { relationCount: { $gt: 0 } } },
  295. { $project: { _id: 1 } },
  296. ]);
  297. if (currentVectorStoreRelationIds.length === 0) {
  298. return;
  299. }
  300. await VectorStoreModel.deleteMany({ _id: { $nin: currentVectorStoreRelationIds }, isDeleted: true });
  301. }
  302. async deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, apiCallInterval?: number): Promise<void> {
  303. // Delete vector store file and delete vector store file relation
  304. const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ vectorStoreRelationId, page: pageId });
  305. if (vectorStoreFileRelation == null) {
  306. return;
  307. }
  308. const deletedFileIds: string[] = [];
  309. for await (const fileId of vectorStoreFileRelation.fileIds) {
  310. try {
  311. const deleteFileResponse = await this.client.deleteFile(fileId);
  312. logger.debug('Delete vector store file', deleteFileResponse);
  313. deletedFileIds.push(fileId);
  314. if (apiCallInterval != null) {
  315. // sleep
  316. await new Promise(resolve => setTimeout(resolve, apiCallInterval));
  317. }
  318. }
  319. catch (err) {
  320. await openaiApiErrorHandler(err, { notFoundError: async() => { deletedFileIds.push(fileId) } });
  321. logger.error(err);
  322. }
  323. }
  324. const undeletedFileIds = vectorStoreFileRelation.fileIds.filter(fileId => !deletedFileIds.includes(fileId));
  325. if (undeletedFileIds.length === 0) {
  326. await vectorStoreFileRelation.remove();
  327. return;
  328. }
  329. vectorStoreFileRelation.fileIds = undeletedFileIds;
  330. await vectorStoreFileRelation.save();
  331. }
  332. async deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void> {
  333. // Retrieves all VectorStore documents that are marked as deleted
  334. const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
  335. if (deletedVectorStoreRelations.length === 0) {
  336. return;
  337. }
  338. // Retrieves VectorStoreFileRelation documents associated with deleted VectorStore documents
  339. const obsoleteVectorStoreFileRelations = await VectorStoreFileRelationModel.find(
  340. { vectorStoreRelationId: { $in: deletedVectorStoreRelations.map(deletedVectorStoreRelation => deletedVectorStoreRelation._id) } },
  341. ).limit(limit);
  342. if (obsoleteVectorStoreFileRelations.length === 0) {
  343. return;
  344. }
  345. // Delete obsolete VectorStoreFile
  346. for await (const vectorStoreFileRelation of obsoleteVectorStoreFileRelations) {
  347. try {
  348. await this.deleteVectorStoreFile(vectorStoreFileRelation.vectorStoreRelationId, vectorStoreFileRelation.page, apiCallInterval);
  349. }
  350. catch (err) {
  351. logger.error(err);
  352. }
  353. }
  354. }
  355. // TODO: https://redmine.weseek.co.jp/issues/160332
  356. // async rebuildVectorStoreAll() {
  357. // await this.deleteVectorStore(VectorStoreScopeType.PUBLIC);
  358. // // Create all public pages VectorStoreFile
  359. // const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
  360. // const pagesStream = Page.find({ grant: PageGrant.GRANT_PUBLIC }).populate('revision').cursor({ batch_size: BATCH_SIZE });
  361. // const batchStrem = createBatchStream(BATCH_SIZE);
  362. // const createVectorStoreFile = this.createVectorStoreFile.bind(this);
  363. // const createVectorStoreFileStream = new Transform({
  364. // objectMode: true,
  365. // async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
  366. // await createVectorStoreFile(chunk);
  367. // this.push(chunk);
  368. // callback();
  369. // },
  370. // });
  371. // await pipeline(pagesStream, batchStrem, createVectorStoreFileStream);
  372. // }
  373. async updateVectorStore(page: HydratedDocument<PageDocument>) {
  374. const pipeline = [
  375. // Stage 1: Match documents with the given pageId
  376. {
  377. $match: {
  378. page: page._id,
  379. },
  380. },
  381. // Stage 2: Lookup VectorStore documents
  382. {
  383. $lookup: {
  384. from: 'vectorstores',
  385. localField: 'vectorStoreRelationId',
  386. foreignField: '_id',
  387. as: 'vectorStore',
  388. },
  389. },
  390. // Stage 3: Unwind the vectorStore array
  391. {
  392. $unwind: '$vectorStore',
  393. },
  394. // Stage 4: Match non-deleted vector stores
  395. {
  396. $match: {
  397. 'vectorStore.isDeleted': false,
  398. },
  399. },
  400. // Stage 5: Replace the root with vectorStore document
  401. {
  402. $replaceRoot: {
  403. newRoot: '$vectorStore',
  404. },
  405. },
  406. ];
  407. const vectorStoreRelations = await VectorStoreFileRelationModel.aggregate<VectorStoreDocument>(pipeline);
  408. vectorStoreRelations.forEach(async(vectorStoreRelation) => {
  409. await this.deleteVectorStoreFile(vectorStoreRelation._id, page._id);
  410. await this.createVectorStoreFile(vectorStoreRelation, [page]);
  411. });
  412. }
  413. private async createVectorStoreFileWithStream(vectorStoreRelation: VectorStoreDocument, conditions: mongoose.FilterQuery<PageDocument>): Promise<void> {
  414. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
  415. const pagesStream = Page.find({ ...conditions })
  416. .populate('revision')
  417. .cursor({ batchSize: BATCH_SIZE });
  418. const batchStream = createBatchStream(BATCH_SIZE);
  419. const createVectorStoreFile = this.createVectorStoreFile.bind(this);
  420. const createVectorStoreFileStream = new Transform({
  421. objectMode: true,
  422. async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
  423. try {
  424. logger.debug('Search results of page paths', chunk.map(page => page.path));
  425. await createVectorStoreFile(vectorStoreRelation, chunk);
  426. this.push(chunk);
  427. callback();
  428. }
  429. catch (error) {
  430. callback(error);
  431. }
  432. },
  433. });
  434. await pipeline(pagesStream, batchStream, createVectorStoreFileStream);
  435. }
  436. private async createConditionForCreateVectorStoreFile(
  437. owner: AiAssistant['owner'],
  438. accessScope: AiAssistant['accessScope'],
  439. grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
  440. pagePathPatterns: AiAssistant['pagePathPatterns'],
  441. ): Promise<mongoose.FilterQuery<PageDocument>> {
  442. const converterdPagePathPatterns = convertPathPatternsToRegExp(pagePathPatterns);
  443. // Include pages in search targets when their paths with 'Anyone with the link' permission are directly specified instead of using glob pattern
  444. const nonGrabPagePathPatterns = pagePathPatterns.filter(pagePathPattern => !isGrobPatternPath(pagePathPattern));
  445. const baseCondition: mongoose.FilterQuery<PageDocument> = {
  446. grant: PageGrant.GRANT_RESTRICTED,
  447. path: { $in: nonGrabPagePathPatterns },
  448. };
  449. if (accessScope === AiAssistantAccessScope.PUBLIC_ONLY) {
  450. return {
  451. $or: [
  452. baseCondition,
  453. {
  454. grant: PageGrant.GRANT_PUBLIC,
  455. path: { $in: converterdPagePathPatterns },
  456. },
  457. ],
  458. };
  459. }
  460. if (accessScope === AiAssistantAccessScope.GROUPS) {
  461. if (grantedGroupsForAccessScope == null || grantedGroupsForAccessScope.length === 0) {
  462. throw new Error('grantedGroups is required when accessScope is GROUPS');
  463. }
  464. const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
  465. return {
  466. $or: [
  467. baseCondition,
  468. {
  469. grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP] },
  470. path: { $in: converterdPagePathPatterns },
  471. $or: [
  472. { 'grantedGroups.item': { $in: extractedGrantedGroupIdsForAccessScope } },
  473. { grant: PageGrant.GRANT_PUBLIC },
  474. ],
  475. },
  476. ],
  477. };
  478. }
  479. if (accessScope === AiAssistantAccessScope.OWNER) {
  480. const ownerUserGroups = [
  481. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  482. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  483. ].map(group => group.toString());
  484. return {
  485. $or: [
  486. baseCondition,
  487. {
  488. grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP, PageGrant.GRANT_OWNER] },
  489. path: { $in: converterdPagePathPatterns },
  490. $or: [
  491. { 'grantedGroups.item': { $in: ownerUserGroups } },
  492. { grantedUsers: { $in: [getIdForRef(owner)] } },
  493. { grant: PageGrant.GRANT_PUBLIC },
  494. ],
  495. },
  496. ],
  497. };
  498. }
  499. throw new Error('Invalid accessScope value');
  500. }
  501. private async validateGrantedUserGroupsForAiAssistant(
  502. owner: AiAssistant['owner'],
  503. shareScope: AiAssistant['shareScope'],
  504. accessScope: AiAssistant['accessScope'],
  505. grantedGroupsForShareScope: AiAssistant['grantedGroupsForShareScope'],
  506. grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
  507. ) {
  508. // Check if grantedGroupsForShareScope is not specified when shareScope is not a “group”
  509. if (shareScope !== AiAssistantShareScope.GROUPS && grantedGroupsForShareScope != null) {
  510. throw new Error('grantedGroupsForShareScope is specified when shareScope is not “groups”.');
  511. }
  512. // Check if grantedGroupsForAccessScope is not specified when accessScope is not a “group”
  513. if (accessScope !== AiAssistantAccessScope.GROUPS && grantedGroupsForAccessScope != null) {
  514. throw new Error('grantedGroupsForAccessScope is specified when accsessScope is not “groups”.');
  515. }
  516. const ownerUserGroupIds = [
  517. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  518. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  519. ].map(group => group.toString());
  520. // Check if the owner belongs to the group specified in grantedGroupsForShareScope
  521. if (grantedGroupsForShareScope != null && grantedGroupsForShareScope.length > 0) {
  522. const extractedGrantedGroupIdsForShareScope = grantedGroupsForShareScope.map(group => getIdForRef(group.item).toString());
  523. const isValid = extractedGrantedGroupIdsForShareScope.every(groupId => ownerUserGroupIds.includes(groupId));
  524. if (!isValid) {
  525. throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForShareScope');
  526. }
  527. }
  528. // Check if the owner belongs to the group specified in grantedGroupsForAccessScope
  529. if (grantedGroupsForAccessScope != null && grantedGroupsForAccessScope.length > 0) {
  530. const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
  531. const isValid = extractedGrantedGroupIdsForAccessScope.every(groupId => ownerUserGroupIds.includes(groupId));
  532. if (!isValid) {
  533. throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForAccessScope');
  534. }
  535. }
  536. }
  537. async createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument> {
  538. await this.validateGrantedUserGroupsForAiAssistant(
  539. data.owner,
  540. data.shareScope,
  541. data.accessScope,
  542. data.grantedGroupsForShareScope,
  543. data.grantedGroupsForAccessScope,
  544. );
  545. const conditions = await this.createConditionForCreateVectorStoreFile(
  546. data.owner,
  547. data.accessScope,
  548. data.grantedGroupsForAccessScope,
  549. data.pagePathPatterns,
  550. );
  551. const vectorStoreRelation = await this.createVectorStore(data.name);
  552. const aiAssistant = await AiAssistantModel.create({
  553. ...data, vectorStore: vectorStoreRelation,
  554. });
  555. // VectorStore creation process does not await
  556. this.createVectorStoreFileWithStream(vectorStoreRelation, conditions);
  557. return aiAssistant;
  558. }
  559. async updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument> {
  560. const aiAssistant = await AiAssistantModel.findOne({ owner: data.owner, _id: aiAssistantId });
  561. if (aiAssistant == null) {
  562. throw createError(404, 'AiAssistant document does not exist');
  563. }
  564. await this.validateGrantedUserGroupsForAiAssistant(
  565. data.owner,
  566. data.shareScope,
  567. data.accessScope,
  568. data.grantedGroupsForShareScope,
  569. data.grantedGroupsForAccessScope,
  570. );
  571. const grantedGroupIdsForAccessScopeFromReq = data.grantedGroupsForAccessScope?.map(group => getIdStringForRef(group.item)) ?? []; // ObjectId[] -> string[]
  572. const grantedGroupIdsForAccessScopeFromDb = aiAssistant.grantedGroupsForAccessScope?.map(group => getIdStringForRef(group.item)) ?? []; // ObjectId[] -> string[]
  573. // If accessScope, pagePathPatterns, grantedGroupsForAccessScope have not changed, do not build VectorStore
  574. const shouldRebuildVectorStore = data.accessScope !== aiAssistant.accessScope
  575. || !isDeepEquals(data.pagePathPatterns, aiAssistant.pagePathPatterns)
  576. || !isDeepEquals(grantedGroupIdsForAccessScopeFromReq, grantedGroupIdsForAccessScopeFromDb);
  577. let newVectorStoreRelation: VectorStoreDocument | undefined;
  578. if (shouldRebuildVectorStore) {
  579. const conditions = await this.createConditionForCreateVectorStoreFile(
  580. data.owner,
  581. data.accessScope,
  582. data.grantedGroupsForAccessScope,
  583. data.pagePathPatterns,
  584. );
  585. // Delete obsoleted VectorStore
  586. const obsoletedVectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
  587. await this.deleteVectorStore(obsoletedVectorStoreRelationId);
  588. newVectorStoreRelation = await this.createVectorStore(data.name);
  589. // VectorStore creation process does not await
  590. this.createVectorStoreFileWithStream(newVectorStoreRelation, conditions);
  591. }
  592. const newData = {
  593. ...data,
  594. vectorStore: newVectorStoreRelation ?? aiAssistant.vectorStore,
  595. };
  596. aiAssistant.set({ ...newData });
  597. const updatedAiAssistant = await aiAssistant.save();
  598. return updatedAiAssistant;
  599. }
  600. async getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants> {
  601. const userGroupIds = [
  602. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  603. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  604. ];
  605. const assistants = await AiAssistantModel.find({
  606. $or: [
  607. // Case 1: Assistants owned by the user
  608. { owner: user },
  609. // Case 2: Public assistants owned by others
  610. {
  611. $and: [
  612. { owner: { $ne: user } },
  613. { shareScope: AiAssistantShareScope.PUBLIC_ONLY },
  614. ],
  615. },
  616. // Case 3: Group-restricted assistants where user is in granted groups
  617. {
  618. $and: [
  619. { owner: { $ne: user } },
  620. { shareScope: AiAssistantShareScope.GROUPS },
  621. { 'grantedGroupsForShareScope.item': { $in: userGroupIds } },
  622. ],
  623. },
  624. ],
  625. })
  626. .populate('grantedGroupsForShareScope.item')
  627. .populate('grantedGroupsForAccessScope.item');
  628. return {
  629. myAiAssistants: assistants.filter(assistant => assistant.owner.toString() === user._id.toString()) ?? [],
  630. teamAiAssistants: assistants.filter(assistant => assistant.owner.toString() !== user._id.toString()) ?? [],
  631. };
  632. }
  633. async deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument> {
  634. const aiAssistant = await AiAssistantModel.findOne({ owner: ownerId, _id: aiAssistantId });
  635. if (aiAssistant == null) {
  636. throw createError(404, 'AiAssistant document does not exist');
  637. }
  638. const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
  639. await this.deleteVectorStore(vectorStoreRelationId);
  640. const deletedAiAssistant = await aiAssistant.remove();
  641. return deletedAiAssistant;
  642. }
  643. }
  644. let instance: OpenaiService;
  645. export const getOpenaiService = (): IOpenaiService | undefined => {
  646. if (instance != null) {
  647. return instance;
  648. }
  649. const aiEnabled = configManager.getConfig('app:aiEnabled');
  650. const openaiServiceType = configManager.getConfig('openai:serviceType');
  651. if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
  652. instance = new OpenaiService();
  653. return instance;
  654. }
  655. return;
  656. };