Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into feat/ai-detail-answ…
Browse files Browse the repository at this point in the history
…er-mode
  • Loading branch information
yuki-takei committed Nov 8, 2024
2 parents 454a6dd + 12fa122 commit 4d0ce7d
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 20 deletions.
1 change: 1 addition & 0 deletions apps/app/public/static/locales/en_US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"not_allowed_to_see_this_page": "You cannot see this page",
"Confirm": "Confirm",
"Successfully requested": "Successfully requested.",
"source": "Source",
"input_validation": {
"target": {
"page_name": "Page name",
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/fr_FR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"not_allowed_to_see_this_page": "Vous ne pouvez pas voir cette page",
"Confirm": "Confirmer",
"Successfully requested": "Demande envoyée.",
"source": "Source",
"input_validation": {
"target": {
"page_name": "Nom de la page",
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/ja_JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"not_allowed_to_see_this_page": "このページは閲覧できません",
"Confirm": "確認",
"Successfully requested": "正常に処理を受け付けました",
"source": "出典",
"input_validation": {
"target": {
"page_name": "ページ名",
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/static/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"Confirm": "确定",
"Successfully requested": "进程成功接受",
"copied_to_clipboard": "它已复制到剪贴板。",
"source": "消息来源",
"input_validation": {
"target": {
"page_name": "页面名称",
Expand Down
8 changes: 4 additions & 4 deletions apps/app/src/components/ReactMarkdownComponents/NextLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Props = Omit<LinkProps, 'href'> & {

export const NextLink = (props: Props): JSX.Element => {
const {
id, href, children, className, ...rest
id, href, children, className, onClick, ...rest
} = props;

const { data: siteUrl } = useSiteUrl();
Expand All @@ -61,7 +61,7 @@ export const NextLink = (props: Props): JSX.Element => {

if (isExternalLink(href, siteUrl)) {
return (
<a id={id} href={href} className={className} target="_blank" rel="noopener noreferrer" {...dataAttributes}>
<a id={id} href={href} className={className} target="_blank" onClick={onClick} rel="noopener noreferrer" {...dataAttributes}>
{children}&nbsp;<span className="growi-custom-icons">external_link</span>
</a>
);
Expand All @@ -70,13 +70,13 @@ export const NextLink = (props: Props): JSX.Element => {
// when href is an anchor link or not-creatable path
if (isAnchorLink(href) || !isCreatablePage(href)) {
return (
<a id={id} href={href} className={className} {...dataAttributes}>{children}</a>
<a id={id} href={href} className={className} onClick={onClick} {...dataAttributes}>{children}</a>
);
}

return (
<Link {...rest} href={href} prefetch={false} legacyBehavior>
<a href={href} className={className} {...dataAttributes}>{children}</a>
<a href={href} className={className} {...dataAttributes} onClick={onClick}>{children}</a>
</Link>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useCallback } from 'react';

import type { LinkProps } from 'next/link';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';

import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';

import { useRagSearchModal } from '../../../client/stores/rag-search';

import styles from './MessageCard.module.scss';

const moduleClass = styles['message-card'] ?? '';
Expand All @@ -19,8 +26,20 @@ const UserMessageCard = ({ children }: { children: string }): JSX.Element => (

const assistantMessageCardModuleClass = styles['assistant-message-card'] ?? '';

const AssistantMessageCard = ({ children }: { children: string }): JSX.Element => {
const NextLinkWrapper = (props: LinkProps & {children: string, href: string}): JSX.Element => {
const { close: closeRagSearchModal } = useRagSearchModal();

const onClick = useCallback(() => {
closeRagSearchModal();
}, [closeRagSearchModal]);

return (
<NextLink href={props.href} onClick={onClick} className="link-primary">
{props.children}
</NextLink>
);
};
const AssistantMessageCard = ({ children }: { children: string }): JSX.Element => {
const { t } = useTranslation();

return (
Expand All @@ -32,7 +51,7 @@ const AssistantMessageCard = ({ children }: { children: string }): JSX.Element =
<div>
{ children.length > 0
? (
<ReactMarkdown>{children}</ReactMarkdown>
<ReactMarkdown components={{ a: NextLinkWrapper }}>{children}</ReactMarkdown>
)
: (
<span className="text-thinking">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getOrCreateModel } from '~/server/util/mongoose-utils';

export interface VectorStoreFileRelation {
vectorStoreRelationId: mongoose.Types.ObjectId;
pageId: mongoose.Types.ObjectId;
page: mongoose.Types.ObjectId;
fileIds: string[];
isAttachedToVectorStore: boolean;
}
Expand All @@ -19,9 +19,9 @@ interface VectorStoreFileRelationModel extends Model<VectorStoreFileRelation> {
}

export const prepareVectorStoreFileRelations = (
vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, fileId: string, relationsMap: Map<string, VectorStoreFileRelation>,
vectorStoreRelationId: Types.ObjectId, page: Types.ObjectId, fileId: string, relationsMap: Map<string, VectorStoreFileRelation>,
): Map<string, VectorStoreFileRelation> => {
const pageIdStr = pageId.toHexString();
const pageIdStr = page.toHexString();
const existingData = relationsMap.get(pageIdStr);

// If the data exists, add the fileId to the fileIds array
Expand All @@ -32,7 +32,7 @@ export const prepareVectorStoreFileRelations = (
else {
relationsMap.set(pageIdStr, {
vectorStoreRelationId,
pageId,
page,
fileIds: [fileId],
isAttachedToVectorStore: false,
});
Expand All @@ -47,7 +47,7 @@ const schema = new Schema<VectorStoreFileRelationDocument, VectorStoreFileRelati
ref: 'VectorStore',
required: true,
},
pageId: {
page: {
type: Schema.Types.ObjectId,
ref: 'Page',
required: true,
Expand All @@ -64,14 +64,14 @@ const schema = new Schema<VectorStoreFileRelationDocument, VectorStoreFileRelati
});

// define unique compound index
schema.index({ vectorStoreRelationId: 1, pageId: 1 }, { unique: true });
schema.index({ vectorStoreRelationId: 1, page: 1 }, { unique: true });

schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRelations: VectorStoreFileRelation[]): Promise<void> {
await this.bulkWrite(
vectorStoreFileRelations.map((data) => {
return {
updateOne: {
filter: { pageId: data.pageId, vectorStoreRelationId: data.vectorStoreRelationId },
filter: { page: data.page, vectorStoreRelationId: data.vectorStoreRelationId },
update: {
$addToSet: { fileIds: { $each: data.fileIds } },
},
Expand All @@ -85,7 +85,7 @@ schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRe
// Used when attached to VectorStore
schema.statics.markAsAttachedToVectorStore = async function(pageIds: Types.ObjectId[]): Promise<void> {
await this.updateMany(
{ pageId: { $in: pageIds } },
{ page: { $in: pageIds } },
{ $set: { isAttachedToVectorStore: true } },
);
};
Expand Down
15 changes: 13 additions & 2 deletions apps/app/src/features/openai/server/routes/message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IUserHasId } from '@growi/core/dist/interfaces';
import { ErrorV3 } from '@growi/core/dist/models';
import type { Request, RequestHandler, Response } from 'express';
import type { ValidationChain } from 'express-validator';
Expand All @@ -15,6 +16,7 @@ import loggerFactory from '~/utils/logger';
import { MessageErrorCode, type StreamErrorCode } from '../../interfaces/message-error';
import { openaiClient } from '../services';
import { getStreamErrorCode } from '../services/getStreamErrorCode';
import { replaceAnnotationWithPageLink } from '../services/replace-annotation-with-page-link';

import { certifyAiService } from './middlewares/certify-ai-service';

Expand All @@ -27,7 +29,9 @@ type ReqBody = {
summaryMode?: boolean,
}

type Req = Request<undefined, Response, ReqBody>
type Req = Request<undefined, Response, ReqBody> & {
user: IUserHasId,
}

type PostMessageHandlersFactory = (crowi: Crowi) => RequestHandler[];

Expand Down Expand Up @@ -86,7 +90,14 @@ export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) =>
'Cache-Control': 'no-cache, no-transform',
});

const messageDeltaHandler = (delta: MessageDelta) => {
const messageDeltaHandler = async(delta: MessageDelta) => {
const content = delta.content?.[0];

// If annotation is found
if (content?.type === 'text' && content?.text?.annotations != null) {
await replaceAnnotationWithPageLink(content, req.user.lang);
}

res.write(`data: ${JSON.stringify(delta)}\n\n`);
};

Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/features/openai/server/services/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class OpenaiService implements IOpenaiService {

async deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, apiCallInterval?: number): Promise<void> {
// Delete vector store file and delete vector store file relation
const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ vectorStoreRelationId, pageId });
const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ vectorStoreRelationId, page: pageId });
if (vectorStoreFileRelation == null) {
return;
}
Expand Down Expand Up @@ -316,7 +316,7 @@ class OpenaiService implements IOpenaiService {
// Delete obsolete VectorStoreFile
for await (const vectorStoreFileRelation of obsoleteVectorStoreFileRelations) {
try {
await this.deleteVectorStoreFile(vectorStoreFileRelation.vectorStoreRelationId, vectorStoreFileRelation.pageId, apiCallInterval);
await this.deleteVectorStoreFile(vectorStoreFileRelation.vectorStoreRelationId, vectorStoreFileRelation.page, apiCallInterval);
}
catch (err) {
logger.error(err);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// See: https://platform.openai.com/docs/assistants/tools/file-search#step-5-create-a-run-and-check-the-output

import type { IPageHasId, Lang } from '@growi/core/dist/interfaces';
import type { MessageContentDelta } from 'openai/resources/beta/threads/messages.mjs';

import VectorStoreFileRelationModel from '~/features/openai/server/models/vector-store-file-relation';
import { getTranslation } from '~/server/service/i18next';

export const replaceAnnotationWithPageLink = async(messageContentDelta: MessageContentDelta, lang?: Lang): Promise<void> => {
if (messageContentDelta?.type === 'text' && messageContentDelta?.text?.annotations != null) {
const annotations = messageContentDelta?.text?.annotations;
for await (const annotation of annotations) {
if (annotation.type === 'file_citation' && annotation.text != null) {

const vectorStoreFileRelation = await VectorStoreFileRelationModel
.findOne({ fileIds: { $in: [annotation.file_citation?.file_id] } })
.populate<{page: Pick<IPageHasId, 'path' | '_id'>}>('page', 'path');

if (vectorStoreFileRelation != null) {
const { t } = await getTranslation(lang);
messageContentDelta.text.value = messageContentDelta.text.value?.replace(
annotation.text,
` [${t('source')}: [${vectorStoreFileRelation.page.path}](/${vectorStoreFileRelation.page._id})]`,
);
}
}
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class ThreadDeletionCronService {
try {
// Random fractional sleep to distribute request timing among GROWI apps
const randomMilliseconds = getRandomIntInRange(0, this.threadDeletionCronMaxMinutesUntilRequest) * 60 * 1000;
this.sleep(randomMilliseconds);
await this.sleep(randomMilliseconds);

await this.executeJob();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class VectorStoreFileDeletionCronService {
try {
// Random fractional sleep to distribute request timing among GROWI apps
const randomMilliseconds = getRandomIntInRange(0, this.vectorStoreFileDeletionCronMaxMinutesUntilRequest) * 60 * 1000;
this.sleep(randomMilliseconds);
await this.sleep(randomMilliseconds);

await this.executeJob();
}
Expand Down
48 changes: 48 additions & 0 deletions apps/app/src/migrations/20241107172359-rename-pageId-to-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import mongoose from 'mongoose';

import VectorStoreFileRelationModel from '~/features/openai/server/models/vector-store-file-relation';
import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
import loggerFactory from '~/utils/logger';


const logger = loggerFactory('growi:migrate:rename-pageId-to-page');

async function dropIndexIfExists(db, collectionName, indexName) {
// check existence of the collection
const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
if (items.length === 0) {
return;
}

const collection = await db.collection(collectionName);
if (await collection.indexExists(indexName)) {
await collection.dropIndex(indexName);
}
}

module.exports = {
async up(db) {
logger.info('Apply migration');
await mongoose.connect(getMongoUri(), mongoOptions);

// Drop index
await dropIndexIfExists(db, 'vectorstorefilerelations', 'vectorStoreRelationId_1_pageId_1');

// Rename field (pageId -> page)
await VectorStoreFileRelationModel.updateMany(
{},
[
{ $set: { page: '$pageId' } },
{ $unset: ['pageId'] },
],
);

// Create index
const collection = mongoose.connection.collection('vectorstorefilerelations');
await collection.createIndex({ vectorStoreRelationId: 1, page: 1 }, { unique: true });
},

async down() {
// No rollback
},
};

0 comments on commit 4d0ce7d

Please sign in to comment.