-
Notifications
You must be signed in to change notification settings - Fork 218
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
Changes from all commits
e8d1667
744f588
9839181
66be0a2
2e9ca2d
adc175f
be160c3
7c5e43f
1d58f5d
f8c5918
fac67f0
5c2de9d
abda9dc
28c128f
0c224e5
caeb96b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
みたいにできないかな? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 以下のようにはできるみたいでした。提示していただいた例とは異なるので一旦そのままにしています。将来的にはあった方がいいかもしれません。 // 基本となるインターフェース(共通のプロパティ)
interface BaseAiAssistant {
name: string;
description: string;
additionalInstruction: string;
pagePathPatterns: string[];
vectorStore: Ref<VectorStore>;
owner: Ref<IUser>;
grantedUsers?: IUser[];
shareScope: AiAssistantShareScope;
}
// publicOnly または owner の場合の型
interface PublicOrOwnerAiAssistant extends BaseAiAssistant {
ownerAccessScope: 'publicOnly' | 'owner';
grantedGroups?: undefined; // undefinedを明示的に指定
}
// groups の場合の型
interface GroupsAiAssistant extends BaseAiAssistant {
ownerAccessScope: 'groups';
grantedGroups: IGrantedGroup[]; // 必須項目として定義
}
// 統合された型
export type AiAssistant = PublicOrOwnerAiAssistant | GroupsAiAssistant; |
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, AiAssistantAccessScope } from '../../interfaces/ai-assistant'; | ||
|
||
/* | ||
* Objects | ||
*/ | ||
const AiAssistantType = { | ||
KNOWLEDGE: 'knowledge', | ||
// EDITOR: 'editor', | ||
// LEARNING: 'learning', | ||
} as const; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
||
|
@@ -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, | ||
}, | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')); | ||
} | ||
}, | ||
]; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. REST 的に post が create の意味を持つから、 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 修正しました |
||
} | ||
|
||
return router; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
} | ||
|
||
let instance: OpenaiService; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
publicOnly, owner の場合は grantedGroups は undefined、
groups の場合のみ grantedGroups に値が入ると思う。
そうすると grantedUsers は不要ではないか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不要でしたので削除しました