Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement AI Assistant creation api #9497

Open
wants to merge 13 commits into
base: feat/growi-ai-next
Choose a base branch
from
38 changes: 38 additions & 0 deletions apps/app/src/features/openai/interfaces/ai-assistant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { IGrantedGroup, IUser, Ref } from '@growi/core';

import type { VectorStore } from '../server/models/vector-store';

/*
* Objects
*/
export const AiAssistantShareScope = {
PUBLIC: 'public',
ONLY_ME: 'onlyMe',
USER_GROUP: 'userGroup',
} as const;

export const AiAssistantOwnerAccessScope = {
PUBLIC: 'public',
ONLY_ME: 'onlyMe',
USER_GROUP: 'userGroup',
} as const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const AiAssistantAccessScope = {
  PUBLIC_ONLY: 'publicOnly',
  OWNER: 'owner
  GROUPS: 'groups',
} as const;

publicOnly, owner の場合は grantedGroups は undefined、
groups の場合のみ grantedGroups に値が入ると思う。

そうすると grantedUsers は不要ではないか?



/*
* Interfaces
*/
export type AiAssistantShareScope = typeof AiAssistantShareScope[keyof typeof AiAssistantShareScope];
export type AiAssistantOwnerAccessScope = typeof AiAssistantOwnerAccessScope[keyof typeof AiAssistantOwnerAccessScope];

export interface AiAssistant {
name: string;
description: string
additionalInstruction: string
pagePathPatterns: string[],
vectorStore: Ref<VectorStore>
owner: Ref<IUser>
grantedUsers?: IUser[]
grantedGroups?: IGrantedGroup[]
shareScope: AiAssistantShareScope
ownerAccessScope: AiAssistantOwnerAccessScope
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  accessScope: AiAssistantAccessScope.public | AiAssistantAccessScope.owner
} |
{
  accessScope:  AiAssistantAccessScope.groups
  grantedGroups: IGrantedGroup[]
}

みたいにできないかな?
interface のまま定義できたかどうか自信ないけれど (type になっちゃうかも?)

56 changes: 3 additions & 53 deletions apps/app/src/features/openai/server/models/ai-assistant.ts
Original file line number Diff line number Diff line change
@@ -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, AiAssistantOwnerAccessScope } from '../../interfaces/ai-assistant';

/*
* Objects
*/
const AiAssistantType = {
KNOWLEDGE: 'knowledge',
// EDITOR: 'editor',
// LEARNING: 'learning',
} as const;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/Web会議室/20250107_GROWI AI Next スコープ決め でアシスタントのデータにこの情報は含めないということになったため削除

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
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apps/app/src/features/openai/interfaces/ai-assistant.ts に移動

interface AiAssistantDocument extends AiAssistant, Document {}
export interface AiAssistantDocument extends AiAssistant, Document {}

type AiAssistantModel = Model<AiAssistantDocument>

Expand Down Expand Up @@ -83,11 +38,6 @@ 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',
Expand Down
113 changes: 113 additions & 0 deletions apps/app/src/features/openai/server/routes/create-ai-assistant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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, AiAssistantOwnerAccessScope } 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'>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • vectorStore は OpenaiService で作成するため omit
  • owner は req.user を入れるため omit


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('grantedUsers')
.optional()
.isArray()
.withMessage('grantedUsers must be an array'),

body('grantedUsers.*') // each item of grantedUsers
.isMongoId()
.withMessage('grantedUsers must be an array mongoId'),

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(AiAssistantOwnerAccessScope))
.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 changes: 4 additions & 0 deletions apps/app/src/features/openai/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const factory = (crowi: Crowi): express.Router => {
import('./message').then(({ postMessageHandlersFactory }) => {
router.post('/message', postMessageHandlersFactory(crowi));
});

import('./create-ai-assistant').then(({ createAiAssistantFactory }) => {
router.post('/assistant', createAiAssistantFactory(crowi));
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REST 的に post が create の意味を持つから、/create-ai-assistant/ai-assistant 等、リソースを意味するものにしていいと思う

}

return router;
Expand Down
10 changes: 9 additions & 1 deletion apps/app/src/features/openai/server/services/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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 {

Expand Down Expand Up @@ -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;
}
Copy link
Member Author

@miya miya Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • AiAssistantDocument の作成ロジック
  • 特化型の VectorStore を作成するロジックは 特化型の VectorStore を作成できる で実装予定
    • AiAssistantModel 作成には vectorStore が必須なのでダミーな vectorStore を入れている


}

let instance: OpenaiService;
Expand Down
Loading