Browse Source

Merge pull request #9290 from weseek/imprv/155273-error-handling-for-when-monthly-usage-limit-is-exceeded

imprv: Error handling for when monthly usage limit is exceeded
mergify[bot] 1 year ago
parent
commit
0ad72cadf0

+ 4 - 1
apps/app/public/static/locales/en_US/translation.json

@@ -491,7 +491,10 @@
     "placeholder": "Ask me anything.",
     "placeholder": "Ask me anything.",
     "caution_against_hallucination": "Please verify the information and check the sources.",
     "caution_against_hallucination": "Please verify the information and check the sources.",
     "progress_label": "Generating answers",
     "progress_label": "Generating answers",
-    "failed_to_create_or_retrieve_thread": "Failed to create or retrieve thread"
+    "failed_to_create_or_retrieve_thread": "Failed to create or retrieve thread",
+    "rate_limit_exceeded": "You have reached your usage limit for OpenAI's API. To use the Knowledge Assistant again, please add credits from the OpenAI billing page.",
+    "rate_limit_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users."
+
   },
   },
   "link_edit": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",

+ 3 - 1
apps/app/public/static/locales/fr_FR/translation.json

@@ -485,7 +485,9 @@
     "placeholder": "Demandez-moi n'importe quoi.",
     "placeholder": "Demandez-moi n'importe quoi.",
     "caution_against_hallucination": "Veuillez vérifier les informations et consulter les sources.",
     "caution_against_hallucination": "Veuillez vérifier les informations et consulter les sources.",
     "progress_label": "Génération des réponses",
     "progress_label": "Génération des réponses",
-    "failed_to_create_or_retrieve_thread": "Échec de la création ou de la récupération du fil de discussion"
+    "failed_to_create_or_retrieve_thread": "Échec de la création ou de la récupération du fil de discussion",
+    "rate_limit_exceeded": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page de facturation d'OpenAI.",
+    "rate_limit_exceeded_for_growi_cloud": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page d'administration de GROWI.cloud pour les utilisateurs hébergés ou à partir de la page de facturation de l'OpenAI pour les utilisateurs propriétaires."
   },
   },
   "link_edit": {
   "link_edit": {
     "edit_link": "Modifier lien",
     "edit_link": "Modifier lien",

+ 3 - 1
apps/app/public/static/locales/ja_JP/translation.json

@@ -524,7 +524,9 @@
     "placeholder": "ききたいことを入力してください",
     "placeholder": "ききたいことを入力してください",
     "caution_against_hallucination": "情報が正しいか出典を確認しましょう",
     "caution_against_hallucination": "情報が正しいか出典を確認しましょう",
     "progress_label": "回答を生成しています",
     "progress_label": "回答を生成しています",
-    "failed_to_create_or_retrieve_thread": "スレッドの作成または取得に失敗しました"
+    "failed_to_create_or_retrieve_thread": "スレッドの作成または取得に失敗しました",
+    "rate_limit_exceeded": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには OpenAI の請求ページからクレジットを追加してください。",
+    "rate_limit_exceeded_for_growi_cloud": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには Hosted の場合は GROWI.cloud の管理画面から Owned の場合は OpenAI の請求ページからクレジットを追加してください。"
   },
   },
   "link_edit": {
   "link_edit": {
     "edit_link": "リンク編集",
     "edit_link": "リンク編集",

+ 3 - 1
apps/app/public/static/locales/zh_CN/translation.json

@@ -480,7 +480,9 @@
     "placeholder": "问我任何问题。",
     "placeholder": "问我任何问题。",
     "caution_against_hallucination": "请核实信息并检查来源。",
     "caution_against_hallucination": "请核实信息并检查来源。",
     "progress_label": "生成答案中",
     "progress_label": "生成答案中",
-    "failed_to_create_or_retrieve_thread": "创建或获取线程失败"
+    "failed_to_create_or_retrieve_thread": "创建或获取线程失败",
+    "rate_limit_exceeded": "您已达到 OpenAI API 的使用上限。要再次使用知识助手,请从 OpenAI 账单页面添加点数。",
+    "rate_limit_exceeded_for_growi_cloud": "您已达到 OpenAI API 使用上限。如需再次使用知识助手,请从GROWI.cloud管理页面为托管用户添加点数,或从OpenAI计费页面为自有用户添加点数。"
   },
   },
   "link_edit": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",

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

@@ -9,10 +9,11 @@ import {
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
+import { useGrowiCloudUri } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { useRagSearchModal } from '../../../client/stores/rag-search';
 import { useRagSearchModal } from '../../../client/stores/rag-search';
-import { MessageErrorCode } from '../../../interfaces/message-error';
+import { MessageErrorCode, StreamErrorCode } from '../../../interfaces/message-error';
 
 
 import { MessageCard } from './MessageCard';
 import { MessageCard } from './MessageCard';
 import { ResizableTextarea } from './ResizableTextArea';
 import { ResizableTextarea } from './ResizableTextArea';
@@ -48,6 +49,8 @@ const AiChatModalSubstance = (): JSX.Element => {
   const [messageLogs, setMessageLogs] = useState<Message[]>([]);
   const [messageLogs, setMessageLogs] = useState<Message[]>([]);
   const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<Message>();
   const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<Message>();
 
 
+  const { data: growiCloudUri } = useGrowiCloudUri();
+
   const isGenerating = generatingAnswerMessage != null;
   const isGenerating = generatingAnswerMessage != null;
 
 
   useEffect(() => {
   useEffect(() => {
@@ -141,14 +144,28 @@ const AiChatModalSubstance = (): JSX.Element => {
 
 
         const chunk = decoder.decode(value);
         const chunk = decoder.decode(value);
 
 
-        // Extract text values from the chunk
-        const textValues = chunk
-          .split('\n\n')
-          .filter(line => line.trim().startsWith('data:'))
-          .map((line) => {
+        const textValues: string[] = [];
+        const lines = chunk.split('\n\n');
+        lines.forEach((line) => {
+          const trimedLine = line.trim();
+          if (trimedLine.startsWith('data:')) {
             const data = JSON.parse(line.replace('data: ', ''));
             const data = JSON.parse(line.replace('data: ', ''));
-            return data.content[0].text.value;
-          });
+            textValues.push(data.content[0].text.value);
+          }
+          else if (trimedLine.startsWith('error:')) {
+            const error = JSON.parse(line.replace('error: ', ''));
+            logger.error(error.errorMessage);
+            form.setError('input', { type: 'manual', message: error.message });
+
+            if (error.code === StreamErrorCode.RATE_LIMIT_EXCEEDED) {
+              const toastErrorMessage = growiCloudUri != null
+                ? 'modal_aichat.rate_limit_exceeded_for_growi_cloud'
+                : 'modal_aichat.rate_limit_exceeded';
+              toastError(t(toastErrorMessage));
+            }
+          }
+        });
+
 
 
         // append text values to the assistant message
         // append text values to the assistant message
         setGeneratingAnswerMessage((prevMessage) => {
         setGeneratingAnswerMessage((prevMessage) => {
@@ -168,7 +185,7 @@ const AiChatModalSubstance = (): JSX.Element => {
       form.setError('input', { type: 'manual', message: err.toString() });
       form.setError('input', { type: 'manual', message: err.toString() });
     }
     }
 
 
-  }, [form, isGenerating, messageLogs, t, threadId]);
+  }, [form, growiCloudUri, isGenerating, messageLogs, t, threadId]);
 
 
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
     if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
     if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {

+ 6 - 0
apps/app/src/features/openai/interfaces/message-error.ts

@@ -1,3 +1,9 @@
 export const MessageErrorCode = {
 export const MessageErrorCode = {
   THREAD_ID_IS_NOT_SET: 'thread-id-is-not-set',
   THREAD_ID_IS_NOT_SET: 'thread-id-is-not-set',
 } as const;
 } as const;
+
+export const StreamErrorCode = {
+  RATE_LIMIT_EXCEEDED: 'rate_limit_exceeded',
+} as const;
+
+export type StreamErrorCode = typeof StreamErrorCode[keyof typeof StreamErrorCode];

+ 13 - 1
apps/app/src/features/openai/server/routes/message.ts

@@ -11,7 +11,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { MessageErrorCode } from '../../interfaces/message-error';
+import { MessageErrorCode, StreamErrorCode } from '../../interfaces/message-error';
 import { openaiClient } from '../services';
 import { openaiClient } from '../services';
 
 
 import { certifyAiService } from './middlewares/certify-ai-service';
 import { certifyAiService } from './middlewares/certify-ai-service';
@@ -80,6 +80,18 @@ export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) =>
         res.write(`data: ${JSON.stringify(delta)}\n\n`);
         res.write(`data: ${JSON.stringify(delta)}\n\n`);
       };
       };
 
 
+      const sendError = (code: StreamErrorCode, message: string) => {
+        res.write(`error: ${JSON.stringify({ code, message })}\n\n`);
+      };
+
+      stream.on('event', (delta) => {
+        if (delta.event === 'thread.run.failed') {
+          if (delta.data.last_error?.code === StreamErrorCode.RATE_LIMIT_EXCEEDED) {
+            logger.error(delta.data.last_error.message);
+            sendError(StreamErrorCode.RATE_LIMIT_EXCEEDED, delta.data.last_error.message);
+          }
+        }
+      });
       stream.on('messageDelta', messageDeltaHandler);
       stream.on('messageDelta', messageDeltaHandler);
       stream.once('messageDone', () => {
       stream.once('messageDone', () => {
         stream.off('messageDelta', messageDeltaHandler);
         stream.off('messageDelta', messageDeltaHandler);