Просмотр исходного кода

Fetch thread messages using OpenAI API

Shun Miyazawa 1 год назад
Родитель
Сommit
214c12ee3c

+ 8 - 8
apps/app/src/features/openai/server/routes/get-messages.ts

@@ -1,7 +1,7 @@
 import { type IUserHasId } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
-import { type ValidationChain, body } from 'express-validator';
+import { type ValidationChain, param } from 'express-validator';
 
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
@@ -17,12 +17,12 @@ const logger = loggerFactory('growi:routes:apiv3:openai:get-message');
 
 type GetMessagesFactory = (crowi: Crowi) => RequestHandler[];
 
-type ReqBody = {
+type ReqParam = {
   threadId: string,
   aiAssistantId: string,
 }
 
-type Req = Request<undefined, Response, ReqBody> & {
+type Req = Request<ReqParam, Response, undefined> & {
   user: IUserHasId,
 }
 
@@ -30,8 +30,8 @@ export const getMessagesFactory: GetMessagesFactory = (crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [
-    body('threadId').isString().withMessage('threadId must be string'),
-    body('aiAssistantId').isMongoId().withMessage('aiAssistantId must be string'),
+    param('threadId').isString().withMessage('threadId must be string'),
+    param('aiAssistantId').isMongoId().withMessage('aiAssistantId must be string'),
   ];
 
   return [
@@ -43,16 +43,16 @@ export const getMessagesFactory: GetMessagesFactory = (crowi) => {
       }
 
       try {
-        const { aiAssistantId } = req.body;
+        const { threadId, aiAssistantId } = req.params;
 
         const isAiAssistantUsable = openaiService.isAiAssistantUsable(aiAssistantId, req.user);
         if (!isAiAssistantUsable) {
           return res.apiv3Err(new ErrorV3('The specified AI assistant is not usable'), 400);
         }
 
-        // TODO: Implement getMessages
+        const messages = await openaiService.getMessageData(threadId, req.user.lang);
 
-        return res.apiv3({});
+        return res.apiv3({ messages });
       }
       catch (err) {
         logger.error(err);

+ 17 - 0
apps/app/src/features/openai/server/services/openai.ts

@@ -2,6 +2,7 @@ import assert from 'node:assert';
 import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 
+import type { Lang } from '@growi/core';
 import {
   PageGrant, getIdForRef, getIdStringForRef, isPopulated, type IUserHasId,
 } from '@growi/core';
@@ -35,6 +36,7 @@ import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
 import { getClient } from './client-delegator';
 // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
 import { openaiApiErrorHandler } from './openai-api-error-handler';
+import { replaceAnnotationWithPageLink } from './replace-annotation-with-page-link';
 
 const { isDeepEquals } = deepEquals;
 
@@ -66,6 +68,7 @@ export interface IOpenaiService {
   // getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
   deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
+  getMessageData(threadId: string, lang?: Lang): Promise<OpenAI.Beta.Threads.Messages.MessagesPage>;
   getVectorStoreRelation(aiAssistantId: string): Promise<VectorStoreDocument>
   getVectorStoreRelationsByPageIds(pageId: Types.ObjectId[]): Promise<VectorStoreDocument[]>;
   createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
@@ -150,6 +153,20 @@ class OpenaiService implements IOpenaiService {
     await ThreadRelationModel.deleteMany({ threadId: { $in: deletedThreadIds } });
   }
 
+  async getMessageData(threadId: string, lang?: Lang): Promise<OpenAI.Beta.Threads.Messages.MessagesPage> {
+    const messages = await this.client.getMessages(threadId);
+
+    for await (const message of messages.data) {
+      for await (const content of message.content) {
+        if (content.type === 'text') {
+          await replaceAnnotationWithPageLink(content, lang);
+        }
+      }
+    }
+
+    return messages;
+  }
+
   // TODO: https://redmine.weseek.co.jp/issues/160332
   // public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
   //   const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: VectorStoreScopeType.PUBLIC, isDeleted: false });