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

Merge pull request #9497 from weseek/feat/159153-implement-ai-assistant-creation-api

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

+ 36 - 0
apps/app/src/features/openai/interfaces/ai-assistant.ts

@@ -0,0 +1,36 @@
+import type { IGrantedGroup, IUser, Ref } from '@growi/core';
+
+import type { VectorStore } from '../server/models/vector-store';
+
+/*
+*  Objects
+*/
+export const AiAssistantShareScope = {
+  PUBLIC_ONLY: 'publicOnly',
+  OWNER: 'owner',
+  GROUPS: 'groups',
+} as const;
+
+export const AiAssistantAccessScope = {
+  PUBLIC_ONLY: 'publicOnly',
+  OWNER: 'owner',
+  GROUPS: 'groups',
+} as const;
+
+/*
+*  Interfaces
+*/
+export type AiAssistantShareScope = typeof AiAssistantShareScope[keyof typeof AiAssistantShareScope];
+export type AiAssistantOwnerAccessScope = typeof AiAssistantAccessScope[keyof typeof AiAssistantAccessScope];
+
+export interface AiAssistant {
+  name: string;
+  description: string
+  additionalInstruction: string
+  pagePathPatterns: string[],
+  vectorStore: Ref<VectorStore>
+  owner: Ref<IUser>
+  grantedGroups?: IGrantedGroup[]
+  shareScope: AiAssistantShareScope
+  ownerAccessScope: AiAssistantOwnerAccessScope
+}

+ 4 - 61
apps/app/src/features/openai/server/models/ai-assistant.ts

@@ -1,56 +1,11 @@
-import {
-  type IGrantedGroup, GroupType, type IUser, type Ref,
-} from '@growi/core';
+import { type IGrantedGroup, GroupType } from '@growi/core';
 import { type Model, type Document, Schema } from 'mongoose';
 
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
-import type { VectorStore } from './vector-store';
+import { type AiAssistant, AiAssistantShareScope, AiAssistantAccessScope } from '../../interfaces/ai-assistant';
 
-/*
-*  Objects
-*/
-const AiAssistantType = {
-  KNOWLEDGE: 'knowledge',
-  // EDITOR: 'editor',
-  // LEARNING: 'learning',
-} as const;
-
-const AiAssistantShareScope = {
-  PUBLIC: 'public',
-  ONLY_ME: 'onlyMe',
-  USER_GROUP: 'userGroup',
-} as const;
-
-const AiAssistantOwnerAccessScope = {
-  PUBLIC: 'public',
-  ONLY_ME: 'onlyMe',
-  USER_GROUP: 'userGroup',
-} as const;
-
-
-/*
-*  Interfaces
-*/
-type AiAssistantType = typeof AiAssistantType[keyof typeof AiAssistantType];
-type AiAssistantShareScope = typeof AiAssistantShareScope[keyof typeof AiAssistantShareScope];
-type AiAssistantOwnerAccessScope = typeof AiAssistantOwnerAccessScope[keyof typeof AiAssistantOwnerAccessScope];
-
-interface AiAssistant {
-  name: string;
-  description: string
-  additionalInstruction: string
-  pagePathPatterns: string[],
-  vectorStore: Ref<VectorStore>
-  types: AiAssistantType[]
-  owner: Ref<IUser>
-  grantedUsers?: IUser[]
-  grantedGroups?: IGrantedGroup[]
-  shareScope: AiAssistantShareScope
-  ownerAccessScope: AiAssistantOwnerAccessScope
-}
-
-interface AiAssistantDocument extends AiAssistant, Document {}
+export interface AiAssistantDocument extends AiAssistant, Document {}
 
 type AiAssistantModel = Model<AiAssistantDocument>
 
@@ -83,23 +38,11 @@ const schema = new Schema<AiAssistantDocument>(
       ref: 'VectorStore',
       required: true,
     },
-    types: [{
-      type: String,
-      enum: Object.values(AiAssistantType),
-      required: true,
-    }],
     owner: {
       type: Schema.Types.ObjectId,
       ref: 'User',
       required: true,
     },
-    grantedUsers: [
-      {
-        type: Schema.Types.ObjectId,
-        ref: 'User',
-        required: true,
-      },
-    ],
     grantedGroups: {
       type: [{
         type: {
@@ -129,7 +72,7 @@ const schema = new Schema<AiAssistantDocument>(
     },
     ownerAccessScope: {
       type: String,
-      enum: Object.values(AiAssistantOwnerAccessScope),
+      enum: Object.values(AiAssistantAccessScope),
       required: true,
     },
   },

+ 103 - 0
apps/app/src/features/openai/server/routes/ai-assistant.ts

@@ -0,0 +1,103 @@
+import { type IUserHasId, GroupType } 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 Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import loggerFactory from '~/utils/logger';
+
+import { type AiAssistant, AiAssistantShareScope, AiAssistantAccessScope } from '../../interfaces/ai-assistant';
+import { getOpenaiService } from '../services/openai';
+
+import { certifyAiService } from './middlewares/certify-ai-service';
+
+const logger = loggerFactory('growi:routes:apiv3:openai:create-ai-assistant');
+
+type CreateAssistantFactory = (crowi: Crowi) => RequestHandler[];
+
+type ReqBody = Omit<AiAssistant, 'vectorStore' | 'owner'>
+
+type Req = Request<undefined, Response, ReqBody> & {
+  user: IUserHasId,
+}
+
+export const createAiAssistantFactory: CreateAssistantFactory = (crowi) => {
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+  const adminRequired = require('~/server/middlewares/admin-required')(crowi);
+
+  const validator: ValidationChain[] = [
+    body('name')
+      .isString()
+      .withMessage('name must be a string')
+      .not()
+      .isEmpty()
+      .withMessage('name is required')
+      .escape(),
+
+    body('description')
+      .optional()
+      .isString()
+      .withMessage('description must be a string')
+      .escape(),
+
+    body('additionalInstruction')
+      .optional()
+      .isString()
+      .withMessage('additionalInstruction must be a string')
+      .escape(),
+
+    body('pagePathPatterns')
+      .isArray()
+      .withMessage('pagePathPatterns must be an array of strings')
+      .not()
+      .isEmpty()
+      .withMessage('pagePathPatterns must not be empty'),
+
+    body('pagePathPatterns.*') // each item of pagePathPatterns
+      .isString()
+      .withMessage('pagePathPatterns must be an array of strings')
+      .notEmpty()
+      .withMessage('pagePathPatterns must not be empty'),
+
+    body('grantedGroups')
+      .optional()
+      .isArray()
+      .withMessage('Granted groups must be an array'),
+
+    body('grantedGroups.*.type') // each item of grantedGroups
+      .isIn(Object.values(GroupType))
+      .withMessage('Invalid grantedGroups type value'),
+
+    body('grantedGroups.*.item') // each item of grantedGroups
+      .isMongoId()
+      .withMessage('Invalid grantedGroups item value'),
+
+    body('shareScope')
+      .isIn(Object.values(AiAssistantShareScope))
+      .withMessage('Invalid shareScope value'),
+
+    body('ownerAccessScope')
+      .isIn(Object.values(AiAssistantAccessScope))
+      .withMessage('Invalid ownerAccessScope value'),
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly, adminRequired, certifyAiService, validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      try {
+        const aiAssistantData = { ...req.body, owner: req.user._id };
+        const openaiService = getOpenaiService();
+        const aiAssistant = await openaiService?.createAiAssistant(aiAssistantData);
+
+        return res.apiv3({ aiAssistant });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3('AiAssistant creation failed'));
+      }
+    },
+  ];
+};

+ 4 - 0
apps/app/src/features/openai/server/routes/index.ts

@@ -30,6 +30,10 @@ export const factory = (crowi: Crowi): express.Router => {
     import('./message').then(({ postMessageHandlersFactory }) => {
       router.post('/message', postMessageHandlersFactory(crowi));
     });
+
+    import('./ai-assistant').then(({ createAiAssistantFactory }) => {
+      router.post('ai-assistant', createAiAssistantFactory(crowi));
+    });
   }
 
   return router;

+ 9 - 1
apps/app/src/features/openai/server/services/openai.ts

@@ -2,7 +2,6 @@ import assert from 'node:assert';
 import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 
-import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { PageGrant, isPopulated } from '@growi/core';
 import type { HydratedDocument, Types } from 'mongoose';
 import mongoose from 'mongoose';
@@ -21,6 +20,8 @@ import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 
 import { OpenaiServiceTypes } from '../../interfaces/ai';
+import { type AiAssistant } from '../../interfaces/ai-assistant';
+import AiAssistantModel, { type AiAssistantDocument } from '../models/ai-assistant';
 import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
 
 import { getClient } from './client-delegator';
@@ -46,6 +47,7 @@ export interface IOpenaiService {
   deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
   rebuildVectorStoreAll(): Promise<void>;
   rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
+  createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
 }
 class OpenaiService implements IOpenaiService {
 
@@ -356,6 +358,12 @@ class OpenaiService implements IOpenaiService {
     await this.createVectorStoreFile([page]);
   }
 
+  async createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument> {
+    const dumyVectorStoreId = '676e0d9863442b736e7ecf09';
+    const aiAssistant = await AiAssistantModel.create({ ...data, vectorStore: dumyVectorStoreId });
+    return aiAssistant;
+  }
+
 }
 
 let instance: OpenaiService;