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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 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,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;
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 は不要ではないか?

Copy link
Member Author

Choose a reason for hiding this comment

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

不要でしたので削除しました


/*
* 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
}
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 になっちゃうかも?)

Copy link
Member Author

Choose a reason for hiding this comment

The 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;

65 changes: 4 additions & 61 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, AiAssistantAccessScope } 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,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: {
Expand Down Expand Up @@ -129,7 +72,7 @@ const schema = new Schema<AiAssistantDocument>(
},
ownerAccessScope: {
type: String,
enum: Object.values(AiAssistantOwnerAccessScope),
enum: Object.values(AiAssistantAccessScope),
required: true,
},
},
Expand Down
103 changes: 103 additions & 0 deletions apps/app/src/features/openai/server/routes/ai-assistant.ts
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'));
}
},
];
};
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('./ai-assistant').then(({ createAiAssistantFactory }) => {
router.post('ai-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 等、リソースを意味するものにしていいと思う

Copy link
Member Author

Choose a reason for hiding this comment

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

修正しました

}

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