openai.ts 45 KB

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