-
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
base: feat/growi-ai-next
Are you sure you want to change the base?
Changes from 12 commits
e8d1667
744f588
9839181
66be0a2
2e9ca2d
adc175f
be160c3
7c5e43f
1d58f5d
f8c5918
fac67f0
5c2de9d
abda9dc
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,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; | ||
|
||
|
||
/* | ||
* 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 | ||
} | ||
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.
みたいにできないかな? |
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; | ||
|
||
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,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', | ||
|
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'> | ||
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.
|
||
|
||
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')); | ||
} | ||
}, | ||
]; | ||
}; |
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('./create-ai-assistant').then(({ createAiAssistantFactory }) => { | ||
router.post('/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 の意味を持つから、 |
||
} | ||
|
||
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 は不要ではないか?