Просмотр исходного кода

Merge branch 'feat/openai-vector-searching' into feat/155197-generate-vector-store-inside-growi

Shun Miyazawa 1 год назад
Родитель
Сommit
5bd551dcb0

+ 2 - 1
apps/app/src/client/components/PageControls/RagSearchButton.module.scss

@@ -1,4 +1,5 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use '@growi/core-styles/scss/variables/growi-official-colors';
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
@@ -8,5 +9,5 @@
 
 // == Colors
 .btn-rag-search {
-  @include btn-muted.colorize(bs.$success);
+  @include btn-muted.colorize(bs.$purple);
 }

+ 1 - 1
apps/app/src/client/components/PageControls/RagSearchButton.tsx

@@ -26,7 +26,7 @@ const RagSearchButton = (): JSX.Element => {
         onClick={ragSearchButtonClickHandler}
         data-testid="open-search-modal-button"
       >
-        <span className="material-symbols-outlined">chat</span>
+        <span className="growi-custom-icons fs-4 align-middle lh-1">knowledge_assistant</span>
       </button>
     </NotAvailableForGuest>
   );

+ 29 - 17
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -18,24 +18,36 @@ const MenuLabel = ({ menu }: { menu: string }) => {
 
   switch (menu) {
     /* eslint-disable no-multi-spaces, max-len */
-    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{        t('headers.app_settings', { ns: 'commons' }) }</>;
-    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{          t('security_settings.security_settings') }</>;
-    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{            t('markdown_settings.markdown_settings') }</>;
-    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{          t('customize_settings.customize_settings') }</>;
-    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{    t('importer_management.import_data') }</>;
-    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{  t('export_management.export_archive_data') }</>;
+    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{         t('headers.app_settings', { ns: 'commons' }) }</>;
+    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{           t('security_settings.security_settings') }</>;
+    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{             t('markdown_settings.markdown_settings') }</>;
+    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{     t('customize_settings.customize_settings') }</>;
+    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{     t('importer_management.import_data') }</>;
+    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{   t('export_management.export_archive_data') }</>;
     case 'data-transfer':            return <><span className="material-symbols-outlined me-1">flight</span>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
-    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{            t('external_notification.external_notification')}</>;
-    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration.slack_integration') }</>;
-    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
-    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{            t('user_management.user_management') }</>;
-    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{          t('user_group_management.user_group_management') }</>;
-    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{            t('audit_log_management.audit_log')}</>;
-    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{          t('plugins.plugins')}</>;
-    case 'ai-integration':           return <><span className="material-symbols-outlined me-1">psychology</span>{          t('ai_integration.ai_integration')}</>;
-    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{       t('full_text_search_management.full_text_search_management') }</>;
-    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{       t('cloud_setting_management.to_cloud_settings')} </>;
-    default:                         return <><span className="material-symbols-outlined me-1">home</span>{            t('wiki_management_homepage') }</>;
+    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{    t('external_notification.external_notification')}</>;
+    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration.slack_integration') }</>;
+    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration_legacy.slack_integration_legacy')}</>;
+    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{           t('user_management.user_management') }</>;
+    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{            t('user_group_management.user_group_management') }</>;
+    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{             t('audit_log_management.audit_log')}</>;
+    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{        t('plugins.plugins')}</>;
+    case 'ai-integration':           return (
+      <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
+        <span
+          className="growi-custom-icons d-inline-block me-1"
+          style={{
+            fontSize: '18px', width: '24px', height: '24px', lineHeight: '24px', verticalAlign: 'bottom', paddingLeft: '2px',
+          }}
+        >
+          growi_ai
+        </span>
+        {t('ai_integration.ai_integration')}
+      </>
+    );
+    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{           t('full_text_search_management.full_text_search_management') }</>;
+    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{            t('cloud_setting_management.to_cloud_settings')} </>;
+    default:                         return <><span className="material-symbols-outlined me-1">home</span>{             t('wiki_management_homepage') }</>;
       /* eslint-enable no-multi-spaces, max-len */
   }
 };

+ 1 - 1
apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx

@@ -235,7 +235,7 @@ export const AiChatModal = (): JSX.Element => {
     <Modal size="lg" isOpen={isOpened} toggle={closeRagSearchModal} className={moduleClass} scrollable>
 
       <ModalHeader tag="h4" toggle={closeRagSearchModal} className="pe-4">
-        <span className="material-symbols-outlined growi-ai-chat-icon me-3">chat</span>
+        <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">knowledge_assistant</span>
         <span className="fw-bold">{t('modal_aichat.title')}</span>
         <span className="fs-5 text-body-secondary ms-3">{t('modal_aichat.title_beta_label')}</span>
       </ModalHeader>

+ 6 - 0
apps/app/src/features/openai/chat/components/AiChatModal/MessageCard.module.scss

@@ -21,6 +21,12 @@
   }
 }
 
+.assistant-message-card :global {
+  .grw-ai-icon {
+    padding: 0.4em;
+  }
+}
+
 // text animation
 // refs: https://web.dev/articles/speedy-css-tip-animated-gradient-text?hl=ja
 .assistant-message-card :global {

+ 11 - 13
apps/app/src/features/openai/chat/components/AiChatModal/MessageCard.tsx

@@ -27,21 +27,19 @@ const AssistantMessageCard = ({ children }: { children: string }): JSX.Element =
     <div className={`card border-0 ${moduleClass} ${assistantMessageCardModuleClass}`}>
       <div className="card-body d-flex">
         <div className="me-2 me-lg-3">
-          <span className="material-symbols-outlined grw-ai-icon rounded-pill p-1">psychology</span>
+          <span className="growi-custom-icons grw-ai-icon rounded-pill">growi_ai</span>
         </div>
 
-        <div className="mt-1">
-          { children.length > 0
-            ? (
-              <ReactMarkdown>{children}</ReactMarkdown>
-            )
-            : (
-              <span className="text-thinking">
-                {t('modal_aichat.progress_label')} <span className="material-symbols-outlined">more_horiz</span>
-              </span>
-            )
-          }
-        </div>
+        { children.length > 0
+          ? (
+            <ReactMarkdown>{children}</ReactMarkdown>
+          )
+          : (
+            <span className="text-thinking">
+              {t('modal_aichat.progress_label')} <span className="material-symbols-outlined">more_horiz</span>
+            </span>
+          )
+        }
       </div>
     </div>
   );

+ 7 - 6
apps/app/src/features/openai/server/models/vector-store-file-relation.ts

@@ -16,23 +16,24 @@ interface VectorStoreFileRelationModel extends Model<VectorStoreFileRelation> {
 }
 
 export const prepareVectorStoreFileRelations = (
-    pageId: Types.ObjectId, fileId: string, vectorStoreFileRelations: VectorStoreFileRelation[],
-): VectorStoreFileRelation[] => {
-  const existingData = vectorStoreFileRelations.find(relation => relation.pageId.equals(pageId));
+    pageId: Types.ObjectId, fileId: string, relationsMap: Map<string, VectorStoreFileRelation>,
+): Map<string, VectorStoreFileRelation> => {
+  const pageIdStr = pageId.toHexString();
+  const existingData = relationsMap.get(pageIdStr);
 
   // If the data exists, add the fileId to the fileIds array
   if (existingData != null) {
     existingData.fileIds.push(fileId);
   }
-  // If the data doesn't exist, create a new one and add it to the array
+  // If the data doesn't exist, create a new one and add it to the map
   else {
-    vectorStoreFileRelations.push({
+    relationsMap.set(pageIdStr, {
       pageId,
       fileIds: [fileId],
     });
   }
 
-  return vectorStoreFileRelations;
+  return relationsMap;
 };
 
 const schema = new Schema<VectorStoreFileRelationDocument, VectorStoreFileRelationModel>({

+ 12 - 5
apps/app/src/server/service/openai/openai.ts

@@ -60,19 +60,19 @@ class OpenaiService implements IOpenaiService {
   }
 
   async createVectorStoreFile(pages: Array<PageDocument>): Promise<void> {
-    const preparedVectorStoreFileRelations: VectorStoreFileRelation[] = [];
+    const vectorStoreFileRelationsMap: Map<string, VectorStoreFileRelation> = new Map();
     const processUploadFile = async(page: PageDocument) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, page.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, preparedVectorStoreFileRelations);
+          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           return;
         }
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, pagePopulatedToShowRevision.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, preparedVectorStoreFileRelations);
+          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
         }
       }
     };
@@ -89,18 +89,25 @@ class OpenaiService implements IOpenaiService {
       }
     });
 
+    const vectorStoreFileRelations = Array.from(vectorStoreFileRelationsMap.values());
+    const uploadedFileIds = vectorStoreFileRelations.map(data => data.fileIds).flat();
     try {
       // Create vector store file
       const vectorStoreId = await this.getOrCreateVectorStoreId();
-      const uploadedFileIds = preparedVectorStoreFileRelations.map(data => data.fileIds).flat();
       const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStoreId, uploadedFileIds);
       logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
 
       // Save vector store file relation
-      await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(preparedVectorStoreFileRelations);
+      await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
     }
     catch (err) {
       logger.error(err);
+
+      // Delete all uploaded files if createVectorStoreFileBatch fails
+      uploadedFileIds.forEach(async(fileId) => {
+        const deleteFileResponse = await this.client.deleteFile(fileId);
+        logger.debug('Delete vector store file (Due to createVectorStoreFileBatch failure)', deleteFileResponse);
+      });
     }
 
   }

+ 1 - 0
packages/custom-icons/svg/growi_ai.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a,.b{fill:none;}.b{fill-rule:evenodd;}</style></defs><g transform="translate(-131 -627)"><path class="b" d="M15.2,2.081a2.084,2.084,0,0,1,4.167,0V17.919A2.089,2.089,0,0,1,17.288,20H15.2ZM3.184,3.372A4.117,4.117,0,0,1,6.56.042H6.539a3.991,3.991,0,0,1,4.522,3.267L13.957,20H11.6a1.785,1.785,0,0,1-1.75-1.436L9.31,15.838H4.705L3.872,20H2.663A2.161,2.161,0,0,1,.538,17.44ZM6.893,5.078,5.518,11.9h3L7.143,5.078A.126.126,0,0,0,6.893,5.078Z" transform="translate(132.5 629)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/knowledge_assistant.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a,.b{fill:none;}.b{fill-rule:evenodd;}</style></defs><g transform="translate(-131 -569)"><path class="b" d="M19.91.6A1.923,1.923,0,0,1,20.5,2.01V19.988l-4-4H2.5A1.927,1.927,0,0,1,1.09,15.4,1.923,1.923,0,0,1,.5,14V2.01A1.923,1.923,0,0,1,1.09.6,1.928,1.928,0,0,1,2.5.012h16A1.928,1.928,0,0,1,19.91.6ZM18.5,2.01H2.5V14H17.35l1.15,1.129ZM13.351,3.2a1,1,0,0,0-1,1v8.6h1a1,1,0,0,0,1-1V4.2A1,1,0,0,0,13.351,3.2Zm-5.15.02a1.976,1.976,0,0,0-1.62,1.6L5.31,11.568A1.037,1.037,0,0,0,6.33,12.8h.58l.4-2H9.52l.26,1.308a.857.857,0,0,0,.84.689h1.13l-1.39-8.01A1.915,1.915,0,0,0,8.19,3.218ZM7.7,8.911l.66-3.276a.061.061,0,0,1,.12,0l.66,3.276Z" transform="translate(132.5 570.988)"/></g></svg>