openai.ts 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  1. import fs from 'fs';
  2. import assert from 'node:assert';
  3. import { Readable, Transform, Writable } from 'stream';
  4. import { pipeline } from 'stream/promises';
  5. import type {
  6. IUser, Ref, Lang, IPage, Nullable,
  7. } from '@growi/core';
  8. import {
  9. PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
  10. } from '@growi/core';
  11. import { deepEquals } from '@growi/core/dist/utils';
  12. import { isGlobPatternPath } from '@growi/core/dist/utils/page-path-utils';
  13. import escapeStringRegexp from 'escape-string-regexp';
  14. import createError from 'http-errors';
  15. import mongoose, { type HydratedDocument, type Types } from 'mongoose';
  16. import { type OpenAI, toFile } from 'openai';
  17. import { type ChatCompletionChunk } from 'openai/resources/chat/completions';
  18. import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
  19. import ThreadRelationModel, { type ThreadRelationDocument } from '~/features/openai/server/models/thread-relation';
  20. import VectorStoreModel, { type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
  21. import VectorStoreFileRelationModel, {
  22. type VectorStoreFileRelation,
  23. prepareVectorStoreFileRelations,
  24. } from '~/features/openai/server/models/vector-store-file-relation';
  25. import type Crowi from '~/server/crowi';
  26. import type { IAttachmentDocument, IAttachmentModel } from '~/server/models/attachment';
  27. import type { PageDocument, PageModel } from '~/server/models/page';
  28. import UserGroupRelation from '~/server/models/user-group-relation';
  29. import { configManager } from '~/server/service/config-manager';
  30. import { createBatchStream } from '~/server/util/batch-stream';
  31. import loggerFactory from '~/utils/logger';
  32. import { OpenaiServiceTypes } from '../../interfaces/ai';
  33. import type { UpsertAiAssistantData } from '../../interfaces/ai-assistant';
  34. import {
  35. type AccessibleAiAssistants, type AiAssistant, AiAssistantAccessScope, AiAssistantShareScope,
  36. } from '../../interfaces/ai-assistant';
  37. import type { MessageListParams } from '../../interfaces/message';
  38. import { ThreadType } from '../../interfaces/thread-relation';
  39. import type { IVectorStore } from '../../interfaces/vector-store';
  40. import { removeGlobPath } from '../../utils/remove-glob-path';
  41. import AiAssistantModel, { type AiAssistantDocument } from '../models/ai-assistant';
  42. import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
  43. import { generateGlobPatterns } from '../utils/generate-glob-patterns';
  44. import { isVectorStoreCompatible } from '../utils/is-vector-store-compatible';
  45. import { getClient, isStreamResponse } from './client-delegator';
  46. import { openaiApiErrorHandler } from './openai-api-error-handler';
  47. import { replaceAnnotationWithPageLink } from './replace-annotation-with-page-link';
  48. const { isDeepEquals } = deepEquals;
  49. const BATCH_SIZE = 100;
  50. const logger = loggerFactory('growi:service:openai');
  51. type VectorStoreFileRelationsMap = Map<string, VectorStoreFileRelation>
  52. const convertPathPatternsToRegExp = (pagePathPatterns: string[]): Array<string | RegExp> => {
  53. return pagePathPatterns.map((pagePathPattern) => {
  54. if (isGlobPatternPath(pagePathPattern)) {
  55. const trimedPagePathPattern = pagePathPattern.replace('/*', '');
  56. const escapedPagePathPattern = escapeStringRegexp(trimedPagePathPattern);
  57. // https://regex101.com/r/x5KIZL/1
  58. return new RegExp(`^${escapedPagePathPattern}($|/)`);
  59. }
  60. return pagePathPattern;
  61. });
  62. };
  63. export interface IOpenaiService {
  64. generateAndProcessPreMessage(message: string, chunkProcessor: (chunk: ChatCompletionChunk) => void): Promise<void>
  65. createThread(userId: string, type: ThreadType, aiAssistantId?: string, initialUserMessage?: string): Promise<ThreadRelationDocument>;
  66. getThreadsByAiAssistantId(aiAssistantId: string): Promise<ThreadRelationDocument[]>
  67. deleteThread(threadRelationId: string): Promise<ThreadRelationDocument>;
  68. deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
  69. deleteObsoletedVectorStoreRelations(): Promise<void> // for CronJob
  70. deleteVectorStore(vectorStoreRelationId: string): Promise<void>;
  71. getMessageData(threadId: string, lang?: Lang, options?: MessageListParams): Promise<OpenAI.Beta.Threads.Messages.MessagesPage>;
  72. createVectorStoreFileOnPageCreate(pages: PageDocument[]): Promise<void>;
  73. updateVectorStoreFileOnPageUpdate(page: HydratedDocument<PageDocument>): Promise<void>;
  74. deleteVectorStoreFilesByPageIds(pageIds: Types.ObjectId[]): Promise<void>;
  75. deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
  76. isAiAssistantUsable(aiAssistantId: string, user: IUserHasId): Promise<boolean>;
  77. createAiAssistant(data: UpsertAiAssistantData, user: IUserHasId): Promise<AiAssistantDocument>;
  78. updateAiAssistant(aiAssistantId: string, data: UpsertAiAssistantData, user: IUserHasId): Promise<AiAssistantDocument>;
  79. getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
  80. isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean>;
  81. }
  82. class OpenaiService implements IOpenaiService {
  83. private crowi: Crowi;
  84. constructor(crowi: Crowi) {
  85. this.crowi = crowi;
  86. this.createVectorStoreFileOnUploadAttachment = this.createVectorStoreFileOnUploadAttachment.bind(this);
  87. crowi.attachmentService.addAttachHandler(this.createVectorStoreFileOnUploadAttachment);
  88. this.deleteVectorStoreFileOnDeleteAttachment = this.deleteVectorStoreFileOnDeleteAttachment.bind(this);
  89. crowi.attachmentService.addDetachHandler(this.deleteVectorStoreFileOnDeleteAttachment);
  90. }
  91. private get client() {
  92. const openaiServiceType = configManager.getConfig('openai:serviceType');
  93. return getClient({ openaiServiceType });
  94. }
  95. async generateAndProcessPreMessage(message: string, chunkProcessor: (delta: ChatCompletionChunk) => void): Promise<void> {
  96. const systemMessage = [
  97. "Generate a message briefly confirming the user's question.",
  98. 'Please generate up to 20 characters',
  99. ].join('');
  100. const preMessageCompletion = await this.client.chatCompletion({
  101. stream: true,
  102. model: 'gpt-4.1-nano',
  103. messages: [
  104. {
  105. role: 'system',
  106. content: systemMessage,
  107. },
  108. {
  109. role: 'user',
  110. content: message,
  111. },
  112. ],
  113. });
  114. if (!isStreamResponse(preMessageCompletion)) {
  115. return;
  116. }
  117. for await (const chunk of preMessageCompletion) {
  118. chunkProcessor(chunk);
  119. }
  120. }
  121. private async generateThreadTitle(message: string): Promise<Nullable<string>> {
  122. const systemMessage = [
  123. 'Create a brief title (max 5 words) from your message.',
  124. 'Respond in the same language the user uses in their input.',
  125. 'Response should only contain the title.',
  126. ].join('');
  127. const threadTitleCompletion = await this.client.chatCompletion({
  128. model: 'gpt-4.1-nano',
  129. messages: [
  130. {
  131. role: 'system',
  132. content: systemMessage,
  133. },
  134. {
  135. role: 'user',
  136. content: message,
  137. },
  138. ],
  139. });
  140. if (!isStreamResponse(threadTitleCompletion)) {
  141. const threadTitle = threadTitleCompletion.choices[0].message.content;
  142. return threadTitle;
  143. }
  144. }
  145. async createThread(userId: string, type: ThreadType, aiAssistantId?: string, initialUserMessage?: string): Promise<ThreadRelationDocument> {
  146. try {
  147. const aiAssistant = aiAssistantId != null
  148. ? await AiAssistantModel.findOne({ _id: { $eq: aiAssistantId } }).populate<{ vectorStore: IVectorStore }>('vectorStore')
  149. : null;
  150. const thread = await this.client.createThread(aiAssistant?.vectorStore?.vectorStoreId);
  151. const threadRelation = await ThreadRelationModel.create({
  152. userId,
  153. type,
  154. aiAssistant: aiAssistantId,
  155. threadId: thread.id,
  156. title: null, // Initialize title as null
  157. });
  158. if (initialUserMessage != null) {
  159. // Do not await, run in background
  160. this.generateThreadTitle(initialUserMessage)
  161. .then(async(generatedTitle) => {
  162. if (generatedTitle != null) {
  163. threadRelation.title = generatedTitle;
  164. await threadRelation.save();
  165. }
  166. })
  167. .catch((err) => {
  168. logger.error(`Failed to generate thread title for threadId ${thread.id}:`, err);
  169. });
  170. }
  171. return threadRelation;
  172. }
  173. catch (err) {
  174. throw err;
  175. }
  176. }
  177. private async updateThreads(aiAssistantId: string, vectorStoreId: string): Promise<void> {
  178. const threadRelations = await this.getThreadsByAiAssistantId(aiAssistantId);
  179. for await (const threadRelation of threadRelations) {
  180. try {
  181. const updatedThreadResponse = await this.client.updateThread(threadRelation.threadId, vectorStoreId);
  182. logger.debug('Update thread', updatedThreadResponse);
  183. }
  184. catch (err) {
  185. logger.error(err);
  186. }
  187. }
  188. }
  189. async getThreadsByAiAssistantId(aiAssistantId: string, type: ThreadType = ThreadType.KNOWLEDGE): Promise<ThreadRelationDocument[]> {
  190. const threadRelations = await ThreadRelationModel.find({ aiAssistant: aiAssistantId, type });
  191. return threadRelations;
  192. }
  193. async deleteThread(threadRelationId: string): Promise<ThreadRelationDocument> {
  194. const threadRelation = await ThreadRelationModel.findById(threadRelationId);
  195. if (threadRelation == null) {
  196. throw createError(404, 'ThreadRelation document does not exist');
  197. }
  198. try {
  199. const deletedThreadResponse = await this.client.deleteThread(threadRelation.threadId);
  200. logger.debug('Delete thread', deletedThreadResponse);
  201. await threadRelation.remove();
  202. }
  203. catch (err) {
  204. await openaiApiErrorHandler(err, { notFoundError: async() => { await threadRelation.remove() } });
  205. throw err;
  206. }
  207. return threadRelation;
  208. }
  209. public async deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void> {
  210. const expiredThreadRelations = await ThreadRelationModel.getExpiredThreadRelations(limit);
  211. if (expiredThreadRelations == null) {
  212. return;
  213. }
  214. const deletedThreadIds: string[] = [];
  215. for await (const expiredThreadRelation of expiredThreadRelations) {
  216. try {
  217. const deleteThreadResponse = await this.client.deleteThread(expiredThreadRelation.threadId);
  218. logger.debug('Delete thread', deleteThreadResponse);
  219. deletedThreadIds.push(expiredThreadRelation.threadId);
  220. // sleep
  221. await new Promise(resolve => setTimeout(resolve, apiCallInterval));
  222. }
  223. catch (err) {
  224. logger.error(err);
  225. }
  226. }
  227. await ThreadRelationModel.deleteMany({ threadId: { $in: deletedThreadIds } });
  228. }
  229. async getMessageData(threadId: string, lang?: Lang, options?: MessageListParams): Promise<OpenAI.Beta.Threads.Messages.MessagesPage> {
  230. const messages = await this.client.getMessages(threadId, options);
  231. for await (const message of messages.data) {
  232. for await (const content of message.content) {
  233. if (content.type === 'text') {
  234. await replaceAnnotationWithPageLink(content, lang);
  235. }
  236. }
  237. }
  238. return messages;
  239. }
  240. private async getVectorStoreRelationsByPageIds(pageIds: Types.ObjectId[]): Promise<VectorStoreDocument[]> {
  241. const pipeline = [
  242. // Stage 1: Match documents with the given pageId
  243. {
  244. $match: {
  245. page: {
  246. $in: pageIds,
  247. },
  248. },
  249. },
  250. // Stage 2: Lookup VectorStore documents
  251. {
  252. $lookup: {
  253. from: 'vectorstores',
  254. localField: 'vectorStoreRelationId',
  255. foreignField: '_id',
  256. as: 'vectorStore',
  257. },
  258. },
  259. // Stage 3: Unwind the vectorStore array
  260. {
  261. $unwind: '$vectorStore',
  262. },
  263. // Stage 4: Match non-deleted vector stores
  264. {
  265. $match: {
  266. 'vectorStore.isDeleted': false,
  267. },
  268. },
  269. // Stage 5: Replace the root with vectorStore document
  270. {
  271. $replaceRoot: {
  272. newRoot: '$vectorStore',
  273. },
  274. },
  275. // Stage 6: Group by _id to remove duplicates
  276. {
  277. $group: {
  278. _id: '$_id',
  279. doc: { $first: '$$ROOT' },
  280. },
  281. },
  282. // Stage 7: Restore the document structure
  283. {
  284. $replaceRoot: {
  285. newRoot: '$doc',
  286. },
  287. },
  288. ];
  289. const vectorStoreRelations = await VectorStoreFileRelationModel.aggregate<VectorStoreDocument>(pipeline);
  290. return vectorStoreRelations;
  291. }
  292. private async createVectorStore(name: string): Promise<VectorStoreDocument> {
  293. try {
  294. const newVectorStore = await this.client.createVectorStore(name);
  295. const newVectorStoreDocument = await VectorStoreModel.create({
  296. vectorStoreId: newVectorStore.id,
  297. }) as VectorStoreDocument;
  298. return newVectorStoreDocument;
  299. }
  300. catch (err) {
  301. throw new Error(err);
  302. }
  303. }
  304. private async uploadFile(revisionBody: string, page: HydratedDocument<PageDocument>): Promise<OpenAI.Files.FileObject> {
  305. const siteUrl = configManager.getConfig('app:siteUrl');
  306. const convertedHtml = await convertMarkdownToHtml(revisionBody, { page, siteUrl });
  307. const file = await toFile(Readable.from(convertedHtml), `${page._id}.html`);
  308. const uploadedFile = await this.client.uploadFile(file);
  309. return uploadedFile;
  310. }
  311. private async uploadFileForAttachment(fileName: string, readStream?: NodeJS.ReadableStream, filePath?: string): Promise<OpenAI.Files.FileObject> {
  312. const streamSource: NodeJS.ReadableStream = (() => {
  313. if (readStream != null) {
  314. return readStream;
  315. }
  316. if (filePath != null) {
  317. return fs.createReadStream(filePath);
  318. }
  319. throw new Error('readStream and filePath are both null');
  320. })();
  321. const uploadableFile = await toFile(
  322. streamSource,
  323. fileName,
  324. );
  325. const uploadedFile = await this.client.uploadFile(uploadableFile);
  326. return uploadedFile;
  327. }
  328. async deleteVectorStore(vectorStoreRelationId: string): Promise<void> {
  329. const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ _id: vectorStoreRelationId, isDeleted: false });
  330. if (vectorStoreDocument == null) {
  331. return;
  332. }
  333. try {
  334. const deleteVectorStoreResponse = await this.client.deleteVectorStore(vectorStoreDocument.vectorStoreId);
  335. logger.debug('Delete vector store', deleteVectorStoreResponse);
  336. await vectorStoreDocument.markAsDeleted();
  337. }
  338. catch (err) {
  339. await openaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
  340. throw new Error(err);
  341. }
  342. }
  343. private async createVectorStoreFileWithStreamForAttachment(
  344. pageId: Types.ObjectId, vectorStoreRelationId: Types.ObjectId, vectorStoreFileRelationsMap: VectorStoreFileRelationsMap,
  345. ): Promise<void> {
  346. const Attachment = mongoose.model<HydratedDocument<IAttachmentDocument>, IAttachmentModel>('Attachment');
  347. const attachmentsCursor = Attachment.find({ page: pageId }).cursor();
  348. const batchStream = createBatchStream(BATCH_SIZE);
  349. const uploadFileStreamForAttachment = new Writable({
  350. objectMode: true,
  351. write: async(attachments: HydratedDocument<IAttachmentDocument>[], _encoding, callback) => {
  352. for await (const attachment of attachments) {
  353. try {
  354. if (!isVectorStoreCompatible(attachment.originalName, attachment.fileFormat)) {
  355. continue;
  356. }
  357. const readStream = await this.crowi.fileUploadService.findDeliveryFile(attachment);
  358. const uploadedFileForAttachment = await this.uploadFileForAttachment(attachment.originalName, readStream);
  359. prepareVectorStoreFileRelations(
  360. vectorStoreRelationId, pageId, uploadedFileForAttachment.id, vectorStoreFileRelationsMap, attachment._id,
  361. );
  362. }
  363. catch (err) {
  364. logger.error(err);
  365. }
  366. }
  367. callback();
  368. },
  369. final: (callback) => {
  370. logger.debug('Finished uploading attachments');
  371. callback();
  372. },
  373. });
  374. await pipeline(attachmentsCursor, batchStream, uploadFileStreamForAttachment);
  375. }
  376. private async createVectorStoreFile(
  377. vectorStoreRelation: VectorStoreDocument, pages: Array<HydratedDocument<PageDocument>>, ignoreAttachments = false,
  378. ): Promise<void> {
  379. const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
  380. const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
  381. if (page._id != null && page.revision != null) {
  382. if (isPopulated(page.revision) && page.revision.body.length > 0) {
  383. const uploadedFile = await this.uploadFile(page.revision.body, page);
  384. prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
  385. if (!ignoreAttachments) {
  386. await this.createVectorStoreFileWithStreamForAttachment(page._id, vectorStoreRelation._id, vectorStoreFileRelationsMap);
  387. }
  388. return;
  389. }
  390. const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
  391. if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
  392. const uploadedFile = await this.uploadFile(pagePopulatedToShowRevision.revision.body, page);
  393. prepareVectorStoreFileRelations(vectorStoreRelation._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
  394. if (!ignoreAttachments) {
  395. await this.createVectorStoreFileWithStreamForAttachment(page._id, vectorStoreRelation._id, vectorStoreFileRelationsMap);
  396. }
  397. }
  398. }
  399. };
  400. // Start workers to process results
  401. const workers = pages.map(processUploadFile);
  402. // Wait for all processing to complete.
  403. assert(workers.length <= BATCH_SIZE, 'workers.length must be less than or equal to BATCH_SIZE');
  404. const fileUploadResult = await Promise.allSettled(workers);
  405. fileUploadResult.forEach((result) => {
  406. if (result.status === 'rejected') {
  407. logger.error(result.reason);
  408. }
  409. });
  410. const vectorStoreFileRelations = Array.from(vectorStoreFileRelationsMap.values());
  411. const uploadedFileIds = vectorStoreFileRelations.map(data => data.fileIds).flat();
  412. if (uploadedFileIds.length === 0) {
  413. return;
  414. }
  415. const pageIds = pages.map(page => page._id);
  416. try {
  417. // Save vector store file relation
  418. await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
  419. // Create vector store file
  420. const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStoreRelation.vectorStoreId, uploadedFileIds);
  421. logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
  422. // Set isAttachedToVectorStore: true when the uploaded file is attached to VectorStore
  423. await VectorStoreFileRelationModel.markAsAttachedToVectorStore(pageIds);
  424. }
  425. catch (err) {
  426. logger.error(err);
  427. // Delete all uploaded files if createVectorStoreFileBatch fails
  428. for await (const pageId of pageIds) {
  429. await this.deleteVectorStoreFile(vectorStoreRelation._id, pageId);
  430. }
  431. }
  432. }
  433. // Deletes all VectorStore documents that are marked as deleted (isDeleted: true) and have no associated VectorStoreFileRelation documents
  434. async deleteObsoletedVectorStoreRelations(): Promise<void> {
  435. const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
  436. if (deletedVectorStoreRelations.length === 0) {
  437. return;
  438. }
  439. const currentVectorStoreRelationIds: Types.ObjectId[] = await VectorStoreFileRelationModel.aggregate([
  440. {
  441. $group: {
  442. _id: '$vectorStoreRelationId',
  443. relationCount: { $sum: 1 },
  444. },
  445. },
  446. { $match: { relationCount: { $gt: 0 } } },
  447. { $project: { _id: 1 } },
  448. ]);
  449. if (currentVectorStoreRelationIds.length === 0) {
  450. return;
  451. }
  452. await VectorStoreModel.deleteMany({ _id: { $nin: currentVectorStoreRelationIds }, isDeleted: true });
  453. }
  454. private async deleteVectorStoreFileForAttachment(vectorStoreFileRelation: VectorStoreFileRelation): Promise<void> {
  455. if (vectorStoreFileRelation.attachment == null) {
  456. return;
  457. }
  458. const deleteAllAttachmentVectorStoreFileRelations = async() => {
  459. await VectorStoreFileRelationModel.deleteMany({ attachment: vectorStoreFileRelation.attachment });
  460. };
  461. try {
  462. // Delete entities in VectorStoreFile
  463. const fileId = vectorStoreFileRelation.fileIds[0];
  464. const deleteFileResponse = await this.client.deleteFile(fileId);
  465. logger.debug('Delete vector store file (attachment) ', deleteFileResponse);
  466. // Delete related VectorStoreFileRelation document
  467. const attachmentId = vectorStoreFileRelation.attachment;
  468. if (attachmentId != null) {
  469. await deleteAllAttachmentVectorStoreFileRelations();
  470. }
  471. }
  472. catch (err) {
  473. logger.error(err);
  474. await openaiApiErrorHandler(err, {
  475. notFoundError: () => deleteAllAttachmentVectorStoreFileRelations(),
  476. });
  477. }
  478. }
  479. private async deleteVectorStoreFile(
  480. vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, ignoreAttachments = false, apiCallInterval?: number,
  481. ): Promise<void> {
  482. if (!ignoreAttachments) {
  483. // Get all VectorStoreFIleDocument (attachments) associated with the page
  484. const vectorStoreFileRelationsForAttachment = await VectorStoreFileRelationModel.find({
  485. vectorStoreRelationId, page: pageId, attachment: { $exists: true },
  486. });
  487. if (vectorStoreFileRelationsForAttachment.length !== 0) {
  488. for await (const vectorStoreFileRelation of vectorStoreFileRelationsForAttachment) {
  489. try {
  490. await this.deleteVectorStoreFileForAttachment(vectorStoreFileRelation);
  491. }
  492. catch (err) {
  493. logger.error(err);
  494. }
  495. }
  496. }
  497. }
  498. // Delete vector store file and delete vector store file relation
  499. const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ vectorStoreRelationId, page: pageId });
  500. if (vectorStoreFileRelation == null) {
  501. return;
  502. }
  503. const deletedFileIds: string[] = [];
  504. for await (const fileId of vectorStoreFileRelation.fileIds) {
  505. try {
  506. const deleteFileResponse = await this.client.deleteFile(fileId);
  507. logger.debug('Delete vector store file', deleteFileResponse);
  508. deletedFileIds.push(fileId);
  509. if (apiCallInterval != null) {
  510. // sleep
  511. await new Promise(resolve => setTimeout(resolve, apiCallInterval));
  512. }
  513. }
  514. catch (err) {
  515. await openaiApiErrorHandler(err, { notFoundError: async() => { deletedFileIds.push(fileId) } });
  516. logger.error(err);
  517. }
  518. }
  519. const undeletedFileIds = vectorStoreFileRelation.fileIds.filter(fileId => !deletedFileIds.includes(fileId));
  520. if (undeletedFileIds.length === 0) {
  521. await vectorStoreFileRelation.remove();
  522. return;
  523. }
  524. vectorStoreFileRelation.fileIds = undeletedFileIds;
  525. await vectorStoreFileRelation.save();
  526. }
  527. async deleteVectorStoreFilesByPageIds(pageIds: Types.ObjectId[]): Promise<void> {
  528. const vectorStoreRelations = await this.getVectorStoreRelationsByPageIds(pageIds);
  529. if (vectorStoreRelations != null && vectorStoreRelations.length !== 0) {
  530. for await (const pageId of pageIds) {
  531. const deleteVectorStoreFilePromises = vectorStoreRelations.map(vectorStoreRelation => this.deleteVectorStoreFile(vectorStoreRelation._id, pageId));
  532. await Promise.allSettled(deleteVectorStoreFilePromises);
  533. }
  534. }
  535. }
  536. async deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void> {
  537. // Retrieves all VectorStore documents that are marked as deleted
  538. const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
  539. if (deletedVectorStoreRelations.length === 0) {
  540. return;
  541. }
  542. // Retrieves VectorStoreFileRelation documents associated with deleted VectorStore documents
  543. const obsoleteVectorStoreFileRelations = await VectorStoreFileRelationModel.find(
  544. { vectorStoreRelationId: { $in: deletedVectorStoreRelations.map(deletedVectorStoreRelation => deletedVectorStoreRelation._id) } },
  545. ).limit(limit);
  546. if (obsoleteVectorStoreFileRelations.length === 0) {
  547. return;
  548. }
  549. // Delete obsolete VectorStoreFile
  550. for await (const vectorStoreFileRelation of obsoleteVectorStoreFileRelations) {
  551. try {
  552. await this.deleteVectorStoreFile(vectorStoreFileRelation.vectorStoreRelationId, vectorStoreFileRelation.page, false, apiCallInterval);
  553. }
  554. catch (err) {
  555. logger.error(err);
  556. }
  557. }
  558. }
  559. private async deleteVectorStoreFileOnDeleteAttachment(attachmentId: string) {
  560. const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ attachment: attachmentId });
  561. if (vectorStoreFileRelation == null) {
  562. return;
  563. }
  564. try {
  565. await this.deleteVectorStoreFileForAttachment(vectorStoreFileRelation);
  566. }
  567. catch (err) {
  568. logger.error(err);
  569. }
  570. }
  571. private async filterPagesByAccessScope(aiAssistant: AiAssistantDocument, pages: HydratedDocument<PageDocument>[]) {
  572. const isPublicPage = (page: HydratedDocument<PageDocument>) => page.grant === PageGrant.GRANT_PUBLIC;
  573. const isUserGroupAccessible = (page: HydratedDocument<PageDocument>, ownerUserGroupIds: string[]) => {
  574. if (page.grant !== PageGrant.GRANT_USER_GROUP) return false;
  575. return page.grantedGroups.some(group => ownerUserGroupIds.includes(getIdStringForRef(group.item)));
  576. };
  577. const isOwnerAccessible = (page: HydratedDocument<PageDocument>, ownerId: Ref<IUser>) => {
  578. if (page.grant !== PageGrant.GRANT_OWNER) return false;
  579. return page.grantedUsers.some(user => getIdStringForRef(user) === getIdStringForRef(ownerId));
  580. };
  581. const getOwnerUserGroupIds = async(owner: Ref<IUser>) => {
  582. const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner);
  583. const externalGroups = await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner);
  584. return [...userGroups, ...externalGroups].map(group => getIdStringForRef(group));
  585. };
  586. switch (aiAssistant.accessScope) {
  587. case AiAssistantAccessScope.PUBLIC_ONLY:
  588. return pages.filter(isPublicPage);
  589. case AiAssistantAccessScope.GROUPS: {
  590. const ownerUserGroupIds = await getOwnerUserGroupIds(aiAssistant.owner);
  591. return pages.filter(page => isPublicPage(page) || isUserGroupAccessible(page, ownerUserGroupIds));
  592. }
  593. case AiAssistantAccessScope.OWNER: {
  594. const ownerUserGroupIds = await getOwnerUserGroupIds(aiAssistant.owner);
  595. return pages.filter(page => isPublicPage(page) || isOwnerAccessible(page, aiAssistant.owner) || isUserGroupAccessible(page, ownerUserGroupIds));
  596. }
  597. default:
  598. return [];
  599. }
  600. }
  601. async createVectorStoreFileOnPageCreate(pages: HydratedDocument<PageDocument>[]): Promise<void> {
  602. const pagePaths = pages.map(page => page.path);
  603. const aiAssistants = await this.findAiAssistantByPagePath(pagePaths, { shouldPopulateOwner: true, shouldPopulateVectorStore: true });
  604. if (aiAssistants.length === 0) {
  605. return;
  606. }
  607. for await (const aiAssistant of aiAssistants) {
  608. if (!isPopulated(aiAssistant.owner)) {
  609. continue;
  610. }
  611. const isLearnablePageLimitExceeded = await this.isLearnablePageLimitExceeded(aiAssistant.owner, aiAssistant.pagePathPatterns);
  612. if (isLearnablePageLimitExceeded) {
  613. continue;
  614. }
  615. const pagesToVectorize = await this.filterPagesByAccessScope(aiAssistant, pages);
  616. const vectorStoreRelation = aiAssistant.vectorStore;
  617. if (vectorStoreRelation == null || !isPopulated(vectorStoreRelation)) {
  618. continue;
  619. }
  620. logger.debug('--------- createVectorStoreFileOnPageCreate ---------');
  621. logger.debug('AccessScopeType of aiAssistant: ', aiAssistant.accessScope);
  622. logger.debug('VectorStoreFile pagePath to be created: ', pagesToVectorize.map(page => page.path));
  623. logger.debug('-----------------------------------------------------');
  624. await this.createVectorStoreFile(vectorStoreRelation as VectorStoreDocument, pagesToVectorize);
  625. }
  626. }
  627. async updateVectorStoreFileOnPageUpdate(page: HydratedDocument<PageDocument>) {
  628. const aiAssistants = await this.findAiAssistantByPagePath([page.path], { shouldPopulateVectorStore: true });
  629. if (aiAssistants.length === 0) {
  630. return;
  631. }
  632. for await (const aiAssistant of aiAssistants) {
  633. const pagesToVectorize = await this.filterPagesByAccessScope(aiAssistant, [page]);
  634. const vectorStoreRelation = aiAssistant.vectorStore;
  635. if (vectorStoreRelation == null || !isPopulated(vectorStoreRelation)) {
  636. continue;
  637. }
  638. logger.debug('---------- updateVectorStoreOnPageUpdate ------------');
  639. logger.debug('AccessScopeType of aiAssistant: ', aiAssistant.accessScope);
  640. logger.debug('PagePath of VectorStoreFile to be deleted: ', page.path);
  641. logger.debug('pagePath of VectorStoreFile to be created: ', pagesToVectorize.map(page => page.path));
  642. logger.debug('-----------------------------------------------------');
  643. // Do not create a new VectorStoreFile if page is changed to a permission that AiAssistant does not have access to
  644. await this.deleteVectorStoreFile(
  645. (vectorStoreRelation as VectorStoreDocument)._id,
  646. page._id,
  647. true, // ignoreAttachments = true
  648. );
  649. await this.createVectorStoreFile(
  650. vectorStoreRelation as VectorStoreDocument,
  651. pagesToVectorize,
  652. true, // ignoreAttachments = true
  653. );
  654. }
  655. }
  656. private async createVectorStoreFileOnUploadAttachment(
  657. pageId: string, attachment: HydratedDocument<IAttachmentDocument>, file: Express.Multer.File,
  658. ): Promise<void> {
  659. if (!isVectorStoreCompatible(file.originalname, file.mimetype)) {
  660. return;
  661. }
  662. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
  663. const page = await Page.findById(pageId);
  664. if (page == null) {
  665. return;
  666. }
  667. const aiAssistants = await this.findAiAssistantByPagePath([page.path], { shouldPopulateVectorStore: true });
  668. if (aiAssistants.length === 0) {
  669. return;
  670. }
  671. const uploadedFile = await this.uploadFileForAttachment(file.originalname, undefined, file.path);
  672. logger.debug('Uploaded file', uploadedFile);
  673. for await (const aiAssistant of aiAssistants) {
  674. const pagesToVectorize = await this.filterPagesByAccessScope(aiAssistant, [page]);
  675. if (pagesToVectorize.length === 0) {
  676. continue;
  677. }
  678. const vectorStoreRelation = aiAssistant.vectorStore;
  679. if (vectorStoreRelation == null || !isPopulated(vectorStoreRelation)) {
  680. continue;
  681. }
  682. await this.client.createVectorStoreFile(vectorStoreRelation.vectorStoreId, uploadedFile.id);
  683. const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
  684. prepareVectorStoreFileRelations(vectorStoreRelation._id as Types.ObjectId, page._id, uploadedFile.id, vectorStoreFileRelationsMap, attachment._id);
  685. const vectorStoreFileRelations = Array.from(vectorStoreFileRelationsMap.values());
  686. await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
  687. }
  688. }
  689. private async createVectorStoreFileWithStream(vectorStoreRelation: VectorStoreDocument, conditions: mongoose.FilterQuery<PageDocument>): Promise<void> {
  690. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
  691. const pagesStream = Page.find({ ...conditions })
  692. .populate('revision')
  693. .cursor({ batchSize: BATCH_SIZE });
  694. const batchStream = createBatchStream(BATCH_SIZE);
  695. const createVectorStoreFile = this.createVectorStoreFile.bind(this);
  696. const createVectorStoreFileStream = new Transform({
  697. objectMode: true,
  698. async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
  699. try {
  700. logger.debug('Target page path for VectorStoreFile generation: ', chunk.map(page => page.path));
  701. await createVectorStoreFile(vectorStoreRelation, chunk);
  702. this.push(chunk);
  703. callback();
  704. }
  705. catch (error) {
  706. callback(error);
  707. }
  708. },
  709. });
  710. await pipeline(pagesStream, batchStream, createVectorStoreFileStream);
  711. }
  712. private async createConditionForCreateVectorStoreFile(
  713. owner: AiAssistant['owner'],
  714. accessScope: AiAssistant['accessScope'],
  715. grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
  716. pagePathPatterns: AiAssistant['pagePathPatterns'],
  717. ): Promise<mongoose.FilterQuery<PageDocument>> {
  718. const convertedPagePathPatterns = convertPathPatternsToRegExp(pagePathPatterns);
  719. // Include pages in search targets when their paths with 'Anyone with the link' permission are directly specified instead of using glob pattern
  720. const nonGrabPagePathPatterns = pagePathPatterns.filter(pagePathPattern => !isGlobPatternPath(pagePathPattern));
  721. const baseCondition: mongoose.FilterQuery<PageDocument> = {
  722. grant: PageGrant.GRANT_RESTRICTED,
  723. path: { $in: nonGrabPagePathPatterns },
  724. };
  725. if (accessScope === AiAssistantAccessScope.PUBLIC_ONLY) {
  726. return {
  727. $or: [
  728. baseCondition,
  729. {
  730. grant: PageGrant.GRANT_PUBLIC,
  731. path: { $in: convertedPagePathPatterns },
  732. },
  733. ],
  734. };
  735. }
  736. if (accessScope === AiAssistantAccessScope.GROUPS) {
  737. if (grantedGroupsForAccessScope == null || grantedGroupsForAccessScope.length === 0) {
  738. throw new Error('grantedGroups is required when accessScope is GROUPS');
  739. }
  740. const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
  741. return {
  742. $or: [
  743. baseCondition,
  744. {
  745. grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP] },
  746. path: { $in: convertedPagePathPatterns },
  747. $or: [
  748. { 'grantedGroups.item': { $in: extractedGrantedGroupIdsForAccessScope } },
  749. { grant: PageGrant.GRANT_PUBLIC },
  750. ],
  751. },
  752. ],
  753. };
  754. }
  755. if (accessScope === AiAssistantAccessScope.OWNER) {
  756. const ownerUserGroups = [
  757. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  758. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  759. ].map(group => group.toString());
  760. return {
  761. $or: [
  762. baseCondition,
  763. {
  764. grant: { $in: [PageGrant.GRANT_PUBLIC, PageGrant.GRANT_USER_GROUP, PageGrant.GRANT_OWNER] },
  765. path: { $in: convertedPagePathPatterns },
  766. $or: [
  767. { 'grantedGroups.item': { $in: ownerUserGroups } },
  768. { grantedUsers: { $in: [getIdForRef(owner)] } },
  769. { grant: PageGrant.GRANT_PUBLIC },
  770. ],
  771. },
  772. ],
  773. };
  774. }
  775. throw new Error('Invalid accessScope value');
  776. }
  777. private async validateGrantedUserGroupsForAiAssistant(
  778. owner: AiAssistant['owner'],
  779. shareScope: AiAssistant['shareScope'],
  780. accessScope: AiAssistant['accessScope'],
  781. grantedGroupsForShareScope: AiAssistant['grantedGroupsForShareScope'],
  782. grantedGroupsForAccessScope: AiAssistant['grantedGroupsForAccessScope'],
  783. ) {
  784. // Check if grantedGroupsForShareScope is not specified when shareScope is not a “group”
  785. if (shareScope !== AiAssistantShareScope.GROUPS && grantedGroupsForShareScope != null) {
  786. throw new Error('grantedGroupsForShareScope is specified when shareScope is not “groups”.');
  787. }
  788. // Check if grantedGroupsForAccessScope is not specified when accessScope is not a “group”
  789. if (accessScope !== AiAssistantAccessScope.GROUPS && grantedGroupsForAccessScope != null) {
  790. throw new Error('grantedGroupsForAccessScope is specified when accsessScope is not “groups”.');
  791. }
  792. const ownerUserGroupIds = [
  793. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  794. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(owner)),
  795. ].map(group => group.toString());
  796. // Check if the owner belongs to the group specified in grantedGroupsForShareScope
  797. if (grantedGroupsForShareScope != null && grantedGroupsForShareScope.length > 0) {
  798. const extractedGrantedGroupIdsForShareScope = grantedGroupsForShareScope.map(group => getIdForRef(group.item).toString());
  799. const isValid = extractedGrantedGroupIdsForShareScope.every(groupId => ownerUserGroupIds.includes(groupId));
  800. if (!isValid) {
  801. throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForShareScope');
  802. }
  803. }
  804. // Check if the owner belongs to the group specified in grantedGroupsForAccessScope
  805. if (grantedGroupsForAccessScope != null && grantedGroupsForAccessScope.length > 0) {
  806. const extractedGrantedGroupIdsForAccessScope = grantedGroupsForAccessScope.map(group => getIdForRef(group.item).toString());
  807. const isValid = extractedGrantedGroupIdsForAccessScope.every(groupId => ownerUserGroupIds.includes(groupId));
  808. if (!isValid) {
  809. throw new Error('A userGroup to which the owner does not belong is specified in grantedGroupsForAccessScope');
  810. }
  811. }
  812. }
  813. async isAiAssistantUsable(aiAssistantId: string, user: IUserHasId): Promise<boolean> {
  814. const aiAssistant = await AiAssistantModel.findOne({ _id: { $eq: aiAssistantId } });
  815. if (aiAssistant == null) {
  816. throw createError(404, 'AiAssistant document does not exist');
  817. }
  818. const isOwner = getIdStringForRef(aiAssistant.owner) === getIdStringForRef(user._id);
  819. if (aiAssistant.shareScope === AiAssistantShareScope.PUBLIC_ONLY) {
  820. return true;
  821. }
  822. if ((aiAssistant.shareScope === AiAssistantShareScope.OWNER) && isOwner) {
  823. return true;
  824. }
  825. if ((aiAssistant.shareScope === AiAssistantShareScope.SAME_AS_ACCESS_SCOPE) && (aiAssistant.accessScope === AiAssistantAccessScope.OWNER) && isOwner) {
  826. return true;
  827. }
  828. if ((aiAssistant.shareScope === AiAssistantShareScope.GROUPS)
  829. || ((aiAssistant.shareScope === AiAssistantShareScope.SAME_AS_ACCESS_SCOPE) && (aiAssistant.accessScope === AiAssistantAccessScope.GROUPS))) {
  830. const userGroupIds = [
  831. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  832. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  833. ].map(group => group.toString());
  834. const grantedGroupIdsForShareScope = aiAssistant.grantedGroupsForShareScope?.map(group => getIdStringForRef(group.item)) ?? [];
  835. const isShared = userGroupIds.some(userGroupId => grantedGroupIdsForShareScope.includes(userGroupId));
  836. return isShared;
  837. }
  838. return false;
  839. }
  840. async createAiAssistant(data: UpsertAiAssistantData, user: IUserHasId): Promise<AiAssistantDocument> {
  841. await this.validateGrantedUserGroupsForAiAssistant(
  842. user,
  843. data.shareScope,
  844. data.accessScope,
  845. data.grantedGroupsForShareScope,
  846. data.grantedGroupsForAccessScope,
  847. );
  848. const conditions = await this.createConditionForCreateVectorStoreFile(
  849. user,
  850. data.accessScope,
  851. data.grantedGroupsForAccessScope,
  852. data.pagePathPatterns,
  853. );
  854. const vectorStoreRelation = await this.createVectorStore(data.name);
  855. const aiAssistant = await AiAssistantModel.create({
  856. ...data, owner: user, vectorStore: vectorStoreRelation,
  857. });
  858. // VectorStore creation process does not await
  859. this.createVectorStoreFileWithStream(vectorStoreRelation, conditions);
  860. return aiAssistant;
  861. }
  862. async updateAiAssistant(aiAssistantId: string, data: UpsertAiAssistantData, user: IUserHasId): Promise<AiAssistantDocument> {
  863. const aiAssistant = await AiAssistantModel.findOne({ owner: user, _id: aiAssistantId });
  864. if (aiAssistant == null) {
  865. throw createError(404, 'AiAssistant document does not exist');
  866. }
  867. await this.validateGrantedUserGroupsForAiAssistant(
  868. user,
  869. data.shareScope,
  870. data.accessScope,
  871. data.grantedGroupsForShareScope,
  872. data.grantedGroupsForAccessScope,
  873. );
  874. const grantedGroupIdsForAccessScopeFromReq = data.grantedGroupsForAccessScope?.map(group => getIdStringForRef(group.item)) ?? []; // ObjectId[] -> string[]
  875. const grantedGroupIdsForAccessScopeFromDb = aiAssistant.grantedGroupsForAccessScope?.map(group => getIdStringForRef(group.item)) ?? []; // ObjectId[] -> string[]
  876. // If accessScope, pagePathPatterns, grantedGroupsForAccessScope have not changed, do not build VectorStore
  877. const shouldRebuildVectorStore = data.accessScope !== aiAssistant.accessScope
  878. || !isDeepEquals(data.pagePathPatterns, aiAssistant.pagePathPatterns)
  879. || !isDeepEquals(grantedGroupIdsForAccessScopeFromReq, grantedGroupIdsForAccessScopeFromDb);
  880. let newVectorStoreRelation: VectorStoreDocument | undefined;
  881. if (shouldRebuildVectorStore) {
  882. const conditions = await this.createConditionForCreateVectorStoreFile(
  883. user,
  884. data.accessScope,
  885. data.grantedGroupsForAccessScope,
  886. data.pagePathPatterns,
  887. );
  888. // Delete obsoleted VectorStore
  889. const obsoletedVectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
  890. await this.deleteVectorStore(obsoletedVectorStoreRelationId);
  891. newVectorStoreRelation = await this.createVectorStore(data.name);
  892. this.updateThreads(aiAssistantId, newVectorStoreRelation.vectorStoreId);
  893. // VectorStore creation process does not await
  894. this.createVectorStoreFileWithStream(newVectorStoreRelation, conditions);
  895. }
  896. const newData = {
  897. ...data,
  898. vectorStore: newVectorStoreRelation ?? aiAssistant.vectorStore,
  899. };
  900. aiAssistant.set({ ...newData });
  901. let updatedAiAssistant: AiAssistantDocument = await aiAssistant.save();
  902. if (data.shareScope !== AiAssistantShareScope.PUBLIC_ONLY && aiAssistant.isDefault) {
  903. updatedAiAssistant = await AiAssistantModel.setDefault(aiAssistant._id, false);
  904. }
  905. return updatedAiAssistant;
  906. }
  907. async getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants> {
  908. const userGroupIds = [
  909. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  910. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  911. ];
  912. const assistants = await AiAssistantModel.find({
  913. $or: [
  914. // Case 1: Assistants owned by the user
  915. { owner: user },
  916. // Case 2: Public assistants owned by others
  917. {
  918. $and: [
  919. { owner: { $ne: user } },
  920. { shareScope: AiAssistantShareScope.PUBLIC_ONLY },
  921. ],
  922. },
  923. // Case 3: Group-restricted assistants where user is in granted groups
  924. {
  925. $and: [
  926. { owner: { $ne: user } },
  927. { shareScope: AiAssistantShareScope.GROUPS },
  928. { 'grantedGroupsForShareScope.item': { $in: userGroupIds } },
  929. ],
  930. },
  931. ],
  932. })
  933. .populate('grantedGroupsForShareScope.item')
  934. .populate('grantedGroupsForAccessScope.item');
  935. return {
  936. myAiAssistants: assistants.filter(assistant => assistant.owner.toString() === user._id.toString()) ?? [],
  937. teamAiAssistants: assistants.filter(assistant => assistant.owner.toString() !== user._id.toString()) ?? [],
  938. };
  939. }
  940. async isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean> {
  941. const normalizedPagePathPatterns = removeGlobPath(pagePathPatterns);
  942. const PageModel = mongoose.model<IPage, PageModel>('Page');
  943. const pagePathsWithDescendantCount = await PageModel.descendantCountByPaths(normalizedPagePathPatterns, user, null, true, true);
  944. const totalPageCount = pagePathsWithDescendantCount.reduce((total, pagePathWithDescendantCount) => {
  945. const descendantCount = pagePathPatterns.includes(pagePathWithDescendantCount.path)
  946. ? 0 // Treat as single page when included in "pagePathPatterns"
  947. : pagePathWithDescendantCount.descendantCount;
  948. const pageCount = descendantCount + 1;
  949. return total + pageCount;
  950. }, 0);
  951. logger.debug('TotalPageCount: ', totalPageCount);
  952. const limitLearnablePageCountPerAssistant = configManager.getConfig('openai:limitLearnablePageCountPerAssistant');
  953. return totalPageCount > limitLearnablePageCountPerAssistant;
  954. }
  955. private async findAiAssistantByPagePath(
  956. pagePaths: string[], options?: { shouldPopulateOwner?: boolean, shouldPopulateVectorStore?: boolean },
  957. ): Promise<AiAssistantDocument[]> {
  958. const pagePathsWithGlobPattern = pagePaths.map(pagePath => generateGlobPatterns(pagePath)).flat();
  959. const query = AiAssistantModel.find({
  960. $or: [
  961. // Case 1: Exact match
  962. { pagePathPatterns: { $in: pagePaths } },
  963. // Case 2: Glob pattern match
  964. { pagePathPatterns: { $in: pagePathsWithGlobPattern } },
  965. ],
  966. });
  967. if (options?.shouldPopulateOwner) {
  968. query.populate('owner');
  969. }
  970. if (options?.shouldPopulateVectorStore) {
  971. query.populate('vectorStore');
  972. }
  973. const aiAssistants = await query.exec();
  974. return aiAssistants;
  975. }
  976. }
  977. let instance: OpenaiService;
  978. export const initializeOpenaiService = (crowi: Crowi): void => {
  979. const aiEnabled = configManager.getConfig('app:aiEnabled');
  980. const openaiServiceType = configManager.getConfig('openai:serviceType');
  981. if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
  982. instance = new OpenaiService(crowi);
  983. }
  984. };
  985. export const getOpenaiService = (): IOpenaiService | undefined => {
  986. if (instance != null) {
  987. return instance;
  988. }
  989. return;
  990. };