Skip to content

Commit

Permalink
Merge of #9399
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Nov 18, 2024
2 parents 11da996 + 8e2fa75 commit 5e19f30
Show file tree
Hide file tree
Showing 42 changed files with 1,157 additions and 469 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/ci-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ on:
- apps/app/**
- '!apps/app/docker/**'
- packages/**
pull_request:
types: [opened, reopened, synchronize]
paths:
- .github/mergify.yml
- .github/workflows/ci-app.yml
- .eslint*
- tsconfig.base.json
- turbo.json
- pnpm-lock.yaml
- package.json
- apps/app/**
- '!apps/app/docker/**'
- packages/**

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
6 changes: 6 additions & 0 deletions apps/app/bin/swagger-jsdoc/definition-apiv3.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ module.exports = {
name: 'access_token',
in: 'query',
},
cookieAuth: {
type: 'apiKey',
in: 'cookie',
name: 'connect.sid',
},
},
},
'x-tagGroups': [
Expand Down Expand Up @@ -57,6 +62,7 @@ module.exports = {
name: 'System Management API',
tags: [
'Home',
'AdminHome',
'AppSettings',
'SecuritySetting',
'MarkDownSetting',
Expand Down
3 changes: 2 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@growi/app",
"version": "7.1.1",
"version": "7.1.2-RC.0",
"license": "MIT",
"private": "true",
"scripts": {
Expand Down Expand Up @@ -257,6 +257,7 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/bunyan": "^1.8.11",
"@types/express": "^4.17.21",
"@types/hast": "^3.0.4",
"@types/jest": "^29.5.2",
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/en_US/commons.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"all": "All",
"unopend": "Unread",
"mark_all_as_read": "Mark all as read",
"no_unread_messages": "no_unread_messages",
"only_unread": "Only unread"
},

Expand Down
3 changes: 2 additions & 1 deletion apps/app/public/static/locales/fr_FR/commons.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"no_notification": "Vous n'avez pas de notifications.",
"all": "Toutes",
"unopend": "Non-lues",
"mark_all_as_read": "Tout marquer comme lu"
"mark_all_as_read": "Tout marquer comme lu",
"no_unread_messages": "aucun message non lu"
},

"personal_dropdown": {
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/ja_JP/commons.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"all": "全て",
"unopend": "未読",
"mark_all_as_read": "全て既読にする",
"no_unread_messages": "未読はありません",
"only_unread": "未読のみ"
},

Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/zh_CN/commons.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"all": "全部",
"unopend": "未读",
"mark_all_as_read" : "标记为已读",
"no_unread_messages": "no_unread_messages",
"only_unread": "Only unread"
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const InAppNotificationDropdown = (): JSX.Element => {
<DropdownMenu end>
{ inAppNotificationData != null && inAppNotificationData.docs.length === 0
// no items
? <DropdownItem disabled>{t('in_app_notification.mark_all_as_read')}</DropdownItem>
? <DropdownItem disabled>{t('in_app_notification.no_unread_messages')}</DropdownItem>
// render DropdownItem
: <InAppNotificationList inAppNotificationData={inAppNotificationData} />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const InAppNotificationPage: FC = () => {
)}
{ notificationData != null && notificationData.docs.length === 0
// no items
? t('in_app_notification.mark_all_as_read')
? t('in_app_notification.no_unread_messages')
// render list-group
: (
<InAppNotificationList inAppNotificationData={notificationData} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,6 @@ const AiChatModalSubstance = (): JSX.Element => {

const isGenerating = generatingAnswerMessage != null;

useEffect(() => {
// do nothing when the modal is closed or threadId is already set
if (threadId != null) {
return;
}

const createThread = async() => {
// create thread
try {
const res = await apiv3Post('/openai/thread');
const thread = res.data.thread;

setThreadId(thread.id);
}
catch (err) {
logger.error(err.toString());
toastError(t('modal_aichat.failed_to_create_or_retrieve_thread'));
}
};

createThread();
}, [t, threadId]);

const submit = useCallback(async(data: FormData) => {
// do nothing when the assistant is generating an answer
if (isGenerating) {
Expand All @@ -107,12 +84,28 @@ const AiChatModalSubstance = (): JSX.Element => {
const newAnswerMessage = { id: (logLength + 1).toString(), content: '' };
setGeneratingAnswerMessage(newAnswerMessage);

// create thread
let currentThreadId = threadId;
if (threadId == null) {
try {
const res = await apiv3Post('/openai/thread');
const thread = res.data.thread;

setThreadId(thread.id);
currentThreadId = thread.id;
}
catch (err) {
logger.error(err.toString());
toastError(t('modal_aichat.failed_to_create_or_retrieve_thread'));
}
}

// post message
try {
const response = await fetch('/_api/v3/openai/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userMessage: data.input, threadId, summaryMode: data.summaryMode }),
body: JSON.stringify({ userMessage: data.input, threadId: currentThreadId, summaryMode: data.summaryMode }),
});

if (!response.ok) {
Expand Down
7 changes: 3 additions & 4 deletions apps/app/src/features/openai/server/models/thread-relation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { addDays } from 'date-fns';
import type mongoose from 'mongoose';
import { type Model, type Document, Schema } from 'mongoose';

import { getOrCreateModel } from '~/server/util/mongoose-utils';

const DAYS_UNTIL_EXPIRATION = 30;
const DAYS_UNTIL_EXPIRATION = 3;

const generateExpirationDate = (): Date => {
const currentDate = new Date();
const expirationDate = new Date(currentDate.setDate(currentDate.getDate() + DAYS_UNTIL_EXPIRATION));
return expirationDate;
return addDays(new Date(), DAYS_UNTIL_EXPIRATION);
};

interface ThreadRelation {
Expand Down
36 changes: 27 additions & 9 deletions apps/app/src/features/openai/server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import { ErrorV3 } from '@growi/core/dist/models';
import express from 'express';

import { postMessageHandlersFactory } from './message';
import { rebuildVectorStoreHandlersFactory } from './rebuild-vector-store';
import { createThreadHandlersFactory } from './thread';
import type Crowi from '~/server/crowi';
import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';

import { isAiEnabled } from '../services';

const router = express.Router();

module.exports = (crowi) => {
router.post('/rebuild-vector-store', rebuildVectorStoreHandlersFactory(crowi));

// create thread
router.post('/thread', createThreadHandlersFactory(crowi));
// post message and return streaming with SSE
router.post('/message', postMessageHandlersFactory(crowi));
export const factory = (crowi: Crowi): express.Router => {

// disable all routes if AI is not enabled
if (!isAiEnabled()) {
router.all('*', (req, res: ApiV3Response) => {
return res.apiv3Err(new ErrorV3('GROWI AI is not enabled'), 501);
});
}
// enabled
else {
import('./rebuild-vector-store').then(({ rebuildVectorStoreHandlersFactory }) => {
router.post('/rebuild-vector-store', rebuildVectorStoreHandlersFactory(crowi));
});

import('./thread').then(({ createThreadHandlersFactory }) => {
router.post('/thread', createThreadHandlersFactory(crowi));
});

import('./message').then(({ postMessageHandlersFactory }) => {
router.post('/message', postMessageHandlersFactory(crowi));
});
}

return router;
};
2 changes: 1 addition & 1 deletion apps/app/src/features/openai/server/routes/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-respo
import loggerFactory from '~/utils/logger';

import { MessageErrorCode, type StreamErrorCode } from '../../interfaces/message-error';
import { openaiClient } from '../services';
import { openaiClient } from '../services/client';
import { getStreamErrorCode } from '../services/getStreamErrorCode';
import { replaceAnnotationWithPageLink } from '../services/replace-annotation-with-page-link';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ const AssistantType = {
CHAT: 'Chat',
} as const;

const AssistantDefaultModelMap: Record<AssistantType, OpenAI.Chat.ChatModel> = {
[AssistantType.SEARCH]: 'gpt-4o-mini',
[AssistantType.CHAT]: 'gpt-4o-mini',
};

const getAssistantModelByType = (type: AssistantType): OpenAI.Chat.ChatModel => {
const configKey = `openai:assistantModel:${type.toLowerCase()}`;
return configManager.getConfig('crowi', configKey) ?? AssistantDefaultModelMap[type];
};

type AssistantType = typeof AssistantType[keyof typeof AssistantType];


Expand All @@ -34,22 +44,23 @@ const findAssistantByName = async(assistantName: string): Promise<OpenAI.Beta.As
return findAssistant(storedAssistants);
};

const getOrCreateAssistant = async(type: AssistantType): Promise<OpenAI.Beta.Assistant> => {
const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Promise<OpenAI.Beta.Assistant> => {
const appSiteUrl = configManager.getConfig('crowi', 'app:siteUrl');
const assistantNameSuffix = configManager.getConfig('crowi', 'openai:assistantNameSuffix');
const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${assistantNameSuffix != null ? ` ${assistantNameSuffix}` : ''}`;
const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${nameSuffix != null ? ` ${nameSuffix}` : ''}`;
const assistantModel = getAssistantModelByType(type);

const assistant = await findAssistantByName(assistantName)
?? (
await openaiClient.beta.assistants.create({
name: assistantName,
model: 'gpt-4o',
model: assistantModel,
}));

// update instructions
const instructions = configManager.getConfig('crowi', 'openai:chatAssistantInstructions');
openaiClient.beta.assistants.update(assistant.id, {
instructions,
model: assistantModel,
tools: [{ type: 'file_search' }],
});

Expand Down
20 changes: 20 additions & 0 deletions apps/app/src/features/openai/server/services/cron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import loggerFactory from '~/utils/logger';

import { isAiEnabled } from '../is-ai-enabled';


const logger = loggerFactory('growi:openai:service:cron');

export const startCronIfEnabled = async(): Promise<void> => {
if (isAiEnabled()) {
logger.info('Starting cron service for thread deletion');
const { ThreadDeletionCronService } = await import('./thread-deletion-cron');
const threadDeletionCronService = new ThreadDeletionCronService();
threadDeletionCronService.startCron();

logger.info('Starting cron service for vector store file deletion');
const { VectorStoreFileDeletionCronService } = await import('./vector-store-file-deletion-cron');
const vectorStoreFileDeletionCronService = new VectorStoreFileDeletionCronService();
vectorStoreFileDeletionCronService.startCron();
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { configManager } from '~/server/service/config-manager';
import loggerFactory from '~/utils/logger';
import { getRandomIntInRange } from '~/utils/rand';

import { getOpenaiService, type IOpenaiService } from './openai';
import { isAiEnabled } from '../is-ai-enabled';
import { getOpenaiService, type IOpenaiService } from '../openai';


const logger = loggerFactory('growi:service:thread-deletion-cron');

class ThreadDeletionCronService {
export class ThreadDeletionCronService {

cronJob: nodeCron.ScheduledTask;

Expand All @@ -25,8 +27,7 @@ class ThreadDeletionCronService {
sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));

startCron(): void {
const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
if (!isAiEnabled) {
if (!isAiEnabled()) {
return;
}

Expand Down Expand Up @@ -67,5 +68,3 @@ class ThreadDeletionCronService {
}

}

export default ThreadDeletionCronService;
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { configManager } from '~/server/service/config-manager';
import loggerFactory from '~/utils/logger';
import { getRandomIntInRange } from '~/utils/rand';

import { getOpenaiService, type IOpenaiService } from './openai';
import { isAiEnabled } from '../is-ai-enabled';
import { getOpenaiService, type IOpenaiService } from '../openai';

const logger = loggerFactory('growi:service:vector-store-file-deletion-cron');

class VectorStoreFileDeletionCronService {
export class VectorStoreFileDeletionCronService {

cronJob: nodeCron.ScheduledTask;

Expand All @@ -25,8 +26,7 @@ class VectorStoreFileDeletionCronService {
sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));

startCron(): void {
const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
if (!isAiEnabled) {
if (!isAiEnabled()) {
return;
}

Expand Down Expand Up @@ -67,5 +67,3 @@ class VectorStoreFileDeletionCronService {
}

}

export default VectorStoreFileDeletionCronService;
3 changes: 1 addition & 2 deletions apps/app/src/features/openai/server/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './embeddings';
export * from './client';
export * from './is-ai-enabled';
3 changes: 3 additions & 0 deletions apps/app/src/features/openai/server/services/is-ai-enabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { configManager } from '~/server/service/config-manager';

export const isAiEnabled = (): boolean => configManager.getConfig('crowi', 'app:aiEnabled');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './normalize-thread-relation-expired-at';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './normalize-thread-relation-expired-at';
Loading

0 comments on commit 5e19f30

Please sign in to comment.