From c32cec7f89b538cd3e12d9ebb95f37872e74bda1 Mon Sep 17 00:00:00 2001 From: tobitege <10787084+tobitege@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:46:58 +0200 Subject: [PATCH] (enh) send status messages to UI during startup (#3771) Co-authored-by: Robert Brennan Co-authored-by: Engel Nyst Co-authored-by: Robert Brennan Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> --- .gitignore | 1 + containers/runtime/README.md | 9 +- frontend/src/components/AgentStatusBar.tsx | 26 +- frontend/src/i18n/translation.json | 859 ++++++++++++++++-- frontend/src/services/actions.ts | 17 +- frontend/src/services/session.ts | 24 +- frontend/src/state/statusSlice.ts | 23 + frontend/src/store.ts | 2 + frontend/src/types/Message.tsx | 9 + frontend/src/types/ResponseType.tsx | 4 +- openhands/core/main.py | 1 - openhands/runtime/client/client.py | 31 + openhands/runtime/client/runtime.py | 37 +- openhands/runtime/e2b/runtime.py | 11 +- openhands/runtime/remote/runtime.py | 6 +- openhands/runtime/runtime.py | 3 + .../session/{agent.py => agent_session.py} | 34 +- openhands/server/session/manager.py | 6 +- openhands/server/session/session.py | 17 +- 19 files changed, 992 insertions(+), 128 deletions(-) create mode 100644 frontend/src/state/statusSlice.ts rename openhands/server/session/{agent.py => agent_session.py} (86%) diff --git a/.gitignore b/.gitignore index fe7501ff2f..5cc736a643 100644 --- a/.gitignore +++ b/.gitignore @@ -228,3 +228,4 @@ runtime_*.tar # docker build containers/runtime/Dockerfile containers/runtime/project.tar.gz +containers/runtime/code diff --git a/containers/runtime/README.md b/containers/runtime/README.md index 5ebf5546e8..d56b0b4825 100644 --- a/containers/runtime/README.md +++ b/containers/runtime/README.md @@ -1,11 +1,12 @@ -# Dynamic constructed Dockerfile +# Dynamically constructed Dockerfile -This folder builds runtime image (sandbox), which will use a `Dockerfile` that is dynamically generated depends on the `base_image` AND a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that's based on the current commit of `openhands`. +This folder builds a runtime image (sandbox), which will use a dynamically generated `Dockerfile` +that depends on the `base_image` **AND** a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that is based on the current commit of `openhands`. -The following command will generate Dockerfile for `ubuntu:22.04` and the source distribution `.tar` into `containers/runtime`. +The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.11-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`: ```bash poetry run python3 openhands/runtime/utils/runtime_build.py \ - --base_image ubuntu:22.04 \ + --base_image nikolaik/python-nodejs:python3.11-nodejs22 \ --build_folder containers/runtime ``` diff --git a/frontend/src/components/AgentStatusBar.tsx b/frontend/src/components/AgentStatusBar.tsx index 9c38e3e20b..7ae2b021c9 100644 --- a/frontend/src/components/AgentStatusBar.tsx +++ b/frontend/src/components/AgentStatusBar.tsx @@ -18,6 +18,7 @@ enum IndicatorColor { function AgentStatusBar() { const { t } = useTranslation(); const { curAgentState } = useSelector((state: RootState) => state.agent); + const { curStatusMessage } = useSelector((state: RootState) => state.status); const AgentStatusMap: { [k: string]: { message: string; indicator: IndicatorColor }; @@ -90,14 +91,25 @@ function AgentStatusBar() { } }, [curAgentState]); + const [statusMessage, setStatusMessage] = React.useState(""); + + React.useEffect(() => { + const trimmedCustomMessage = curStatusMessage.message.trim(); + if (trimmedCustomMessage) { + setStatusMessage(t(trimmedCustomMessage)); + } else { + setStatusMessage(AgentStatusMap[curAgentState].message); + } + }, [curAgentState, curStatusMessage.message]); + return ( -
-
- - {AgentStatusMap[curAgentState].message} - +
+
+
+ {statusMessage} +
); } diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 8d1f2617fa..795c60e051 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -72,19 +72,43 @@ "en": "Options", "zh-CN": "选项", "zh-TW": "選項", - "de": "Optionen" + "de": "Optionen", + "ko-KR": "옵션", + "no": "Alternativer", + "it": "Opzioni", + "pt": "Opções", + "es": "Opciones", + "ar": "خيارات", + "fr": "Options", + "tr": "Seçenekler" }, "CODE_EDITOR$FILE_SAVE_ERROR": { "en": "An unknown error occurred while saving the file", "zh-CN": "文件保存时发生未知错误", "zh-TW": "文件保存時發生未知錯誤", - "de": "Beim Speichern der Datei ist ein unbekannter Fehler aufgetreten" + "de": "Beim Speichern der Datei ist ein unbekannter Fehler aufgetreten", + "ko-KR": "파일 저장 중 알 수 없는 오류가 발생했습니다", + "no": "En ukjent feil oppstod under lagring av filen", + "it": "Si è verificato un errore sconosciuto durante il salvataggio del file", + "pt": "Ocorreu um erro desconhecido ao salvar o arquivo", + "es": "Ocurrió un error desconocido al guardar el archivo", + "ar": "حدث خطأ غير معروف أثناء حفظ الملف", + "fr": "Une erreur inconnue s'est produite lors de l'enregistrement du fichier", + "tr": "Dosya kaydedilirken bilinmeyen bir hata oluştu" }, "CODE_EDITOR$EMPTY_MESSAGE": { "en": "No file selected.", "zh-CN": "文件未选中", "zh-TW": "未選取任何文件。", - "de": "Keine Datei ausgewählt." + "de": "Keine Datei ausgewählt.", + "ko-KR": "선택된 파일이 없습니다.", + "no": "Ingen fil valgt.", + "it": "Nessun file selezionato.", + "pt": "Nenhum arquivo selecionado.", + "es": "Ningún archivo seleccionado.", + "ar": "لم يتم اختيار أي ملف.", + "fr": "Aucun fichier sélectionné.", + "tr": "Hiçbir dosya seçilmedi." }, "FILE_SERVICE$SELECT_FILE_ERROR": { "en": "Error selecting file. Please try again.", @@ -336,12 +360,30 @@ "CONFIGURATION$SECURITY_SELECT_LABEL": { "en": "Security analyzer", "de": "Sicherheitsanalysator", - "zh-CN": "安全分析器" + "zh-CN": "安全分析器", + "ko-KR": "보안 분석기", + "no": "Sikkerhetsanalysator", + "zh-TW": "安全分析器", + "it": "Analizzatore di sicurezza", + "pt": "Analisador de segurança", + "es": "Analizador de seguridad", + "ar": "محلل الأمان", + "fr": "Analyseur de sécurité", + "tr": "Güvenlik analizörü" }, "CONFIGURATION$SECURITY_SELECT_PLACEHOLDER": { "en": "Select a security analyzer (optional)", "de": "Wählen Sie einen Sicherheitsanalysator (optional)", - "zh-CN": "选择一个安全分析器(可选)" + "zh-CN": "选择一个安全分析器(可选)", + "ko-KR": "보안 분석기 선택 (선택사항)", + "no": "Velg en sikkerhetsanalysator (valgfritt)", + "zh-TW": "選擇安全分析器(可選)", + "it": "Seleziona un analizzatore di sicurezza (opzionale)", + "pt": "Selecione um analisador de segurança (opcional)", + "es": "Seleccione un analizador de seguridad (opcional)", + "ar": "اختر محلل أمان (اختياري)", + "fr": "Sélectionnez un analyseur de sécurité (facultatif)", + "tr": "Bir güvenlik analizörü seçin (isteğe bağlı)" }, "CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": { "en": "Close", @@ -386,100 +428,268 @@ }, "CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE": { "en": "We've changed some settings in the latest update. Take a minute to review.", - "de": "Mit dem letzten Update haben wir ein paar Einstellungen geändert. Bitte kontrollieren Ihre Einstellungen." + "de": "Mit dem letzten Update haben wir ein paar Einstellungen geändert. Bitte kontrollieren Ihre Einstellungen.", + "zh-CN": "我们在最新更新中更改了一些设置。请花点时间检查一下。", + "ko-KR": "최신 업데이트에서 일부 설정을 변경했습니다. 잠시 시간을 내어 검토해 주세요.", + "no": "Vi har endret noen innstillinger i den siste oppdateringen. Ta deg tid til å se gjennom dem.", + "zh-TW": "我們在最新更新中更改了一些設定。請花點時間檢查一下。", + "it": "Abbiamo modificato alcune impostazioni nell'ultimo aggiornamento. Prenditi un momento per rivederle.", + "pt": "Alteramos algumas configurações na última atualização. Reserve um momento para revisar.", + "es": "Hemos cambiado algunas configuraciones en la última actualización. Tómate un momento para revisarlas.", + "ar": "لقد قمنا بتغيير بعض الإعدادات في التحديث الأخير. خذ دقيقة لمراجعتها.", + "fr": "Nous avons modifié certains paramètres dans la dernière mise à jour. Prenez un moment pour les examiner.", + "tr": "Son güncellemede bazı ayarları değiştirdik. Gözden geçirmek için bir dakikanızı ayırın." }, "CONFIGURATION$AGENT_LOADING": { - "en": "Please wait while the agent loads. This may take a few seconds...", - "de": "Bitte warten Sie, während der Agent lädt. Das kann ein paar Sekunden dauern..." + "en": "Please wait while the agent loads. This may take a few minutes...", + "de": "Bitte warten Sie, während der Agent lädt. Das kann ein paar Minuten dauern...", + "zh-CN": "请稍候,代理正在加载中。这可能需要几分钟...", + "ko-KR": "에이전트가 로드되는 동안 기다려 주세요. 몇 분 정도 걸릴 수 있습니다...", + "no": "Vennligst vent mens agenten laster. Dette kan ta noen minutter...", + "zh-TW": "請稍候,代理正在載入中。這可能需要幾分鐘...", + "it": "Attendere mentre l'agente si carica. Potrebbe richiedere alcuni minuti...", + "pt": "Por favor, aguarde enquanto o agente carrega. Isso pode levar alguns minutos...", + "es": "Por favor, espere mientras el agente se carga. Esto puede tardar unos minutos...", + "ar": "يرجى الانتظار أثناء تحميل الوكيل. قد يستغرق هذا بضع دقائق...", + "fr": "Veuillez patienter pendant le chargement de l'agent. Cela peut prendre quelques minutes...", + "tr": "Lütfen ajan yüklenirken bekleyin. Bu birkaç dakika sürebilir..." }, "CONFIGURATION$AGENT_RUNNING": { "en": "Please stop the agent before editing these settings.", - "de": "Bitte beenden Sie den Agenten vor der Bearbeitung der Einstellungen." + "de": "Bitte beenden Sie den Agenten vor der Bearbeitung der Einstellungen.", + "zh-CN": "请在编辑这些设置之前停止代理。", + "ko-KR": "이 설정을 편집하기 전에 에이전트를 중지해 주세요.", + "no": "Vennligst stopp agenten før du redigerer disse innstillingene.", + "zh-TW": "請在編輯這些設定之前停止代理。", + "it": "Si prega di fermare l'agente prima di modificare queste impostazioni.", + "pt": "Por favor, pare o agente antes de editar estas configurações.", + "es": "Por favor, detenga el agente antes de editar estas configuraciones.", + "ar": "يرجى إيقاف الوكيل قبل تعديل هذه الإعدادات.", + "fr": "Veuillez arrêter l'agent avant de modifier ces paramètres.", + "tr": "Bu ayarları düzenlemeden önce lütfen ajanı durdurun." }, "CONFIGURATION$ERROR_FETCH_MODELS": { "en": "Failed to fetch models and agents", "zh-CN": "获取模型和智能体失败", - "de": "Fehler beim Abrufen der Modelle und Agenten" + "de": "Fehler beim Abrufen der Modelle und Agenten", + "zh-TW": "獲取模型和智能體失敗", + "es": "Error al obtener modelos y agentes", + "fr": "Échec de la récupération des modèles et des agents", + "it": "Impossibile recuperare modelli e agenti", + "pt": "Falha ao buscar modelos e agentes", + "ko-KR": "모델 및 에이전트 가져오기 실패", + "ar": "فشل في جلب النماذج والوكلاء", + "tr": "Modeller ve ajanlar getirilemedi", + "no": "Kunne ikke hente modeller og agenter" }, "SESSION$SERVER_CONNECTED_MESSAGE": { "en": "Connected to server", "zh-CN": "已连接到服务器", - "de": "Verbindung zum Server hergestellt" + "de": "Verbindung zum Server hergestellt", + "zh-TW": "已連接到伺服器", + "es": "Conectado al servidor", + "fr": "Connecté au serveur", + "it": "Connesso al server", + "pt": "Conectado ao servidor", + "ko-KR": "서버에 연결됨", + "ar": "تم الاتصال بالخادم", + "tr": "Sunucuya bağlandı", + "no": "Koblet til server" }, "SESSION$SESSION_HANDLING_ERROR_MESSAGE": { "en": "Error handling message", "zh-CN": "处理消息时发生错误", - "de": "Fehler beim Verarbeiten der Nachricht" + "de": "Fehler beim Verarbeiten der Nachricht", + "zh-TW": "處理訊息時發生錯誤", + "es": "Error al procesar el mensaje", + "fr": "Erreur lors du traitement du message", + "it": "Errore durante l'elaborazione del messaggio", + "pt": "Erro ao processar a mensagem", + "ko-KR": "메시지 처리 중 오류 발생", + "ar": "خطأ في معالجة الرسالة", + "tr": "Mesaj işlenirken hata oluştu", + "no": "Feil ved behandling av melding" }, "SESSION$SESSION_CONNECTION_ERROR_MESSAGE": { "en": "Error connecting to session", "zh-CN": "连接到会话时发生错误", - "de": "Verbindung zur Sitzung fehlgeschlagen" + "de": "Verbindung zur Sitzung fehlgeschlagen", + "zh-TW": "連接到會話時發生錯誤", + "es": "Error al conectar con la sesión", + "fr": "Erreur de connexion à la session", + "it": "Errore durante la connessione alla sessione", + "pt": "Erro ao conectar à sessão", + "ko-KR": "세션 연결 오류", + "ar": "خطأ في الاتصال بالجلسة", + "tr": "Oturuma bağlanırken hata oluştu", + "no": "Feil ved tilkobling til økt" }, "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE": { "en": "Socket not initialized", "zh-CN": "Socket 未初始化", - "de": "Socket nicht initialisiert" + "de": "Socket nicht initialisiert", + "zh-TW": "Socket 未初始化", + "es": "Socket no inicializado", + "fr": "Socket non initialisé", + "it": "Socket non inizializzato", + "pt": "Socket não inicializado", + "ko-KR": "소켓이 초기화되지 않았습니다", + "ar": "لم يتم تهيئة Socket", + "tr": "Soket başlatılmadı" }, "EXPLORER$UPLOAD_ERROR_MESSAGE": { "en": "Error uploading file", "zh-CN": "上传文件时发生错误", - "de": "Fehler beim Hochladen der Datei" + "de": "Fehler beim Hochladen der Datei", + "zh-TW": "上傳檔案時發生錯誤", + "es": "Error al subir el archivo", + "fr": "Erreur lors du téléchargement du fichier", + "it": "Errore durante il caricamento del file", + "pt": "Erro ao fazer upload do arquivo", + "ko-KR": "파일 업로드 중 오류 발생", + "ar": "خطأ في تحميل الملف", + "tr": "Dosya yüklenirken hata oluştu" }, "EXPLORER$LABEL_DROP_FILES": { "en": "Drop files here", "zh-CN": "将文件拖到这里", - "de": "Dateien hier ablegen" + "de": "Dateien hier ablegen", + "zh-TW": "將檔案拖曳至此", + "es": "Suelta los archivos aquí", + "fr": "Déposez les fichiers ici", + "it": "Trascina i file qui", + "pt": "Solte os arquivos aqui", + "ko-KR": "파일을 여기에 놓으세요", + "ar": "أسقط الملفات هنا", + "tr": "Dosyaları buraya bırakın" }, "EXPLORER$LABEL_WORKSPACE": { "en": "Workspace", "zh-CN": "工作区", - "de": "Arbeitsbereich" + "de": "Arbeitsbereich", + "zh-TW": "工作區", + "es": "Espacio de trabajo", + "fr": "Espace de travail", + "it": "Area di lavoro", + "pt": "Espaço de trabalho", + "ko-KR": "작업 공간", + "ar": "مساحة العمل", + "tr": "Çalışma alanı" }, "EXPLORER$EMPTY_WORKSPACE_MESSAGE": { "en": "No files in workspace", "zh-CN": "工作区没有文件", - "de": "Keine Dateien im Arbeitsbereich" + "de": "Keine Dateien im Arbeitsbereich", + "zh-TW": "工作區沒有檔案", + "es": "No hay archivos en el espacio de trabajo", + "fr": "Aucun fichier dans l'espace de travail", + "it": "Nessun file nell'area di lavoro", + "pt": "Nenhum arquivo no espaço de trabalho", + "ko-KR": "작업 공간에 파일이 없습니다", + "ar": "لا توجد ملفات في مساحة العمل", + "tr": "Çalışma alanında dosya yok" }, "EXPLORER$LOADING_WORKSPACE_MESSAGE": { "en": "Loading workspace...", "zh-CN": "正在加载工作区...", - "de": "Arbeitsbereich wird geladen..." + "de": "Arbeitsbereich wird geladen...", + "zh-TW": "正在載入工作區...", + "es": "Cargando espacio de trabajo...", + "fr": "Chargement de l'espace de travail...", + "it": "Caricamento dell'area di lavoro...", + "pt": "Carregando espaço de trabalho...", + "ko-KR": "작업 공간 로딩 중...", + "ar": "جارٍ تحميل مساحة العمل...", + "tr": "Çalışma alanı yükleniyor..." }, "EXPLORER$REFRESH_ERROR_MESSAGE": { "en": "Error refreshing workspace", "zh-CN": "工作区刷新错误", - "de": "Fehler beim Aktualisieren des Arbeitsbereichs" + "de": "Fehler beim Aktualisieren des Arbeitsbereichs", + "zh-TW": "工作區重新整理錯誤", + "es": "Error al actualizar el espacio de trabajo", + "fr": "Erreur lors de l'actualisation de l'espace de travail", + "it": "Errore durante l'aggiornamento dell'area di lavoro", + "pt": "Erro ao atualizar o espaço de trabalho", + "ko-KR": "작업 공간 새로 고침 오류", + "ar": "خطأ في تحديث مساحة العمل", + "tr": "Çalışma alanı yenilenirken hata oluştu" }, "EXPLORER$UPLOAD_SUCCESS_MESSAGE": { "en": "Successfully uploaded {{count}} file(s)", "zh-CN": "成功上传 {{count}} 个文件", - "de": "Erfolgreich {{count}} Datei(en) hochgeladen" + "de": "Erfolgreich {{count}} Datei(en) hochgeladen", + "zh-TW": "成功上傳 {{count}} 個檔案", + "es": "Se subieron {{count}} archivo(s) con éxito", + "fr": "{{count}} fichier(s) téléchargé(s) avec succès", + "it": "Caricato con successo {{count}} file", + "pt": "{{count}} arquivo(s) carregado(s) com sucesso", + "ko-KR": "{{count}}개의 파일을 성공적으로 업로드했습니다", + "ar": "تم تحميل {{count}} ملف (ملفات) بنجاح", + "tr": "{{count}} dosya başarıyla yüklendi" }, "EXPLORER$NO_FILES_UPLOADED_MESSAGE": { "en": "No files were uploaded", "zh-CN": "没有文件上传", - "de": "Keine Dateien wurden hochgeladen" + "de": "Keine Dateien wurden hochgeladen", + "zh-TW": "沒有檔案被上傳", + "es": "No se subieron archivos", + "fr": "Aucun fichier n'a été téléchargé", + "it": "Nessun file è stato caricato", + "pt": "Nenhum arquivo foi carregado", + "ko-KR": "업로드된 파일이 없습니다", + "ar": "لم يتم تحميل أي ملفات", + "tr": "Hiçbir dosya yüklenmedi" }, "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE": { "en": "{{count}} file(s) were skipped during upload", + "de": "{{count}} Datei(en) wurden während des Hochladens übersprungen", "zh-CN": "{{count}} 个文件在上传过程中被跳过", - "de": "{{count}} Datei(en) wurden während des Hochladens übersprungen" + "zh-TW": "{{count}} 個檔案在上傳過程中被跳過", + "es": "Se omitieron {{count}} archivo(s) durante la carga", + "fr": "{{count}} fichier(s) ont été ignorés pendant le téléchargement", + "it": "{{count}} file sono stati saltati durante il caricamento", + "pt": "{{count}} arquivo(s) foram ignorados durante o upload", + "ko-KR": "업로드 중 {{count}}개의 파일이 건너뛰어졌습니다", + "ar": "تم تخطي {{count}} ملف (ملفات) أثناء التحميل", + "tr": "Yükleme sırasında {{count}} dosya atlandı" }, "EXPLORER$UPLOAD_UNEXPECTED_RESPONSE_MESSAGE": { "en": "Unexpected response structure from server", "zh-CN": "服务器响应结构不符合预期", - "de": "Unerwartetes Antwortformat vom Server" + "de": "Unerwartetes Antwortformat vom Server", + "zh-TW": "伺服器回應結構不符合預期", + "es": "Estructura de respuesta inesperada del servidor", + "fr": "Structure de réponse inattendue du serveur", + "it": "Struttura di risposta inaspettata dal server", + "pt": "Estrutura de resposta inesperada do servidor", + "ko-KR": "서버로부터 예상치 못한 응답 구조", + "ar": "بنية استجابة غير متوقعة من الخادم", + "tr": "Sunucudan beklenmeyen yanıt yapısı" }, "LOAD_SESSION$MODAL_TITLE": { "en": "Return to existing session?", "de": "Zurück zu vorhandener Sitzung?", "zh-CN": "是否继续未完成的会话?", - "zh-TW": "是否繼續未完成的會話?" + "zh-TW": "是否繼續未完成的會話?", + "es": "¿Volver a la sesión existente?", + "fr": "Revenir à la session existante ?", + "it": "Tornare alla sessione esistente?", + "pt": "Retornar à sessão existente?", + "ko-KR": "기존 세션으로 돌아가시겠습니까?", + "ar": "العودة إلى الجلسة الحالية؟", + "tr": "Mevcut oturuma dönmek ister misiniz?" }, "LOAD_SESSION$MODAL_CONTENT": { "en": "You seem to have an ongoing session. Would you like to pick up where you left off, or start fresh?", "de": "Sie haben eine aktive Sitzung. Möchten Sie die Arbeit an der vorherigen Stelle fortsetzen oder von vorne anfangen?", + "es": "Parece que tienes una sesión en curso. ¿Te gustaría continuar donde lo dejaste o empezar de nuevo?", + "fr": "Il semble que vous ayez une session en cours. Souhaitez-vous reprendre là où vous vous êtes arrêté ou recommencer à zéro ?", + "it": "Sembra che tu abbia una sessione in corso. Vorresti riprendere da dove hai lasciato o ricominciare da capo?", + "pt": "Parece que você tem uma sessão em andamento. Gostaria de continuar de onde parou ou começar do zero?", + "ko-KR": "진행 중인 세션이 있는 것 같습니다. 중단한 곳에서 계속하시겠습니까, 아니면 새로 시작하시겠습니까?", + "ar": "يبدو أن لديك جلسة جارية. هل ترغب في استكمال ما توقفت عنده أم البدء من جديد؟", + "tr": "Devam eden bir oturumunuz var gibi görünüyor. Kaldığınız yerden devam etmek mi yoksa yeniden başlamak mı istersiniz?", "zh-CN": "您似乎有一个未完成的任务。您想继续之前的工作还是重新开始?", "zh-TW": "您似乎有一個未完成的任務。您想從上次離開的地方繼續還是重新開始?" }, @@ -487,103 +697,276 @@ "en": "Resume Session", "de": "Sitzung fortsetzen", "zh-CN": "恢复会话", - "zh-TW": "恢復會話" + "zh-TW": "恢復會話", + "es": "Reanudar sesión", + "fr": "Reprendre la session", + "it": "Riprendi sessione", + "pt": "Retomar sessão", + "ko-KR": "세션 재개", + "ar": "استئناف الجلسة", + "tr": "Oturumu Devam Ettir" }, "LOAD_SESSION$START_NEW_SESSION_MODAL_ACTION_LABEL": { "en": "Start New Session", "de": "Neue Sitzung starten", "zh-CN": "开始新会话", - "zh-TW": "開始新會話" + "zh-TW": "開始新會話", + "es": "Iniciar nueva sesión", + "fr": "Démarrer une nouvelle session", + "it": "Avvia nuova sessione", + "pt": "Iniciar nova sessão", + "ko-KR": "새 세션 시작", + "ar": "بدء جلسة جديدة", + "tr": "Yeni Oturum Başlat" }, "FEEDBACK$MODAL_TITLE": { - "en": "Share feedback" + "en": "Share feedback", + "de": "Feedback teilen", + "zh-CN": "分享反馈", + "zh-TW": "分享反饋", + "es": "Compartir comentarios", + "fr": "Partager des commentaires", + "it": "Condividi feedback", + "pt": "Compartilhar feedback", + "ko-KR": "피드백 공유", + "ar": "مشاركة التعليقات", + "tr": "Geri bildirim paylaş" }, "FEEDBACK$MODAL_CONTENT": { - "en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data." + "en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data.", + "de": "Um uns zu verbessern, sammeln wir Feedback aus Ihren Interaktionen, um unsere Prompts zu verbessern. Durch das Absenden dieses Formulars stimmen Sie der Erfassung dieser Daten zu.", + "zh-CN": "为了帮助我们改进,我们会收集您的互动反馈以改进我们的提示。提交此表单即表示您同意我们收集这些数据。", + "zh-TW": "為了幫助我們改進,我們會收集您的互動反饋以改進我們的提示。提交此表單即表示您同意我們收集這些數據。", + "es": "Para ayudarnos a mejorar, recopilamos comentarios de sus interacciones para mejorar nuestras indicaciones. Al enviar este formulario, usted consiente que recopilemos estos datos.", + "fr": "Pour nous aider à nous améliorer, nous recueillons des commentaires de vos interactions pour améliorer nos invites. En soumettant ce formulaire, vous consentez à ce que nous collections ces données.", + "it": "Per aiutarci a migliorare, raccogliamo feedback dalle tue interazioni per migliorare i nostri prompt. Inviando questo modulo, acconsenti alla raccolta di questi dati.", + "pt": "Para nos ajudar a melhorar, coletamos feedback de suas interações para aprimorar nossas sugestões. Ao enviar este formulário, você consente que coletemos esses dados.", + "ko-KR": "개선을 위해 귀하의 상호 작용에서 피드백을 수집하여 프롬프트를 개선합니다. 이 양식을 제출함으로써 귀하는 이 데이터 수집에 동의하게 됩니다.", + "ar": "لمساعدتنا على التحسين، نقوم بجمع التعليقات من تفاعلاتك لتحسين مطالباتنا. من خلال إرسال هذا النموذج، فإنك توافق على جمعنا لهذه البيانات.", + "tr": "Kendimizi geliştirmemize yardımcı olmak için, etkileşimlerinizden geri bildirim toplayarak ipuçlarımızı iyileştiriyoruz. Bu formu göndererek, bu verileri toplamamıza izin vermiş olursunuz." }, "FEEDBACK$EMAIL_LABEL": { - "en": "Your email" + "en": "Your email", + "de": "Ihre E-Mail-Adresse", + "zh-CN": "您的电子邮箱", + "zh-TW": "您的電子郵箱", + "es": "Su correo electrónico", + "fr": "Votre e-mail", + "it": "La tua email", + "pt": "Seu e-mail", + "ko-KR": "귀하의 이메일", + "ar": "بريدك الإلكتروني", + "tr": "E-posta adresiniz" }, "FEEDBACK$CONTRIBUTE_LABEL": { - "en": "Contribute to public dataset" + "en": "Contribute to public dataset", + "de": "Zum öffentlichen Datensatz beitragen", + "zh-CN": "贡献到公共数据集", + "zh-TW": "貢獻到公共數據集", + "es": "Contribuir al conjunto de datos público", + "fr": "Contribuer à l'ensemble de données public", + "it": "Contribuisci al dataset pubblico", + "pt": "Contribuir para o conjunto de dados público", + "ko-KR": "공개 데이터셋에 기여", + "ar": "المساهمة في مجموعة البيانات العامة", + "tr": "Genel veri setine katkıda bulun" }, "FEEDBACK$SHARE_LABEL": { - "en": "Share" + "en": "Share", + "de": "Teilen", + "zh-CN": "分享", + "zh-TW": "分享", + "es": "Compartir", + "fr": "Partager", + "it": "Condividi", + "pt": "Compartilhar", + "ko-KR": "공유", + "ar": "مشاركة", + "tr": "Paylaş" }, "FEEDBACK$CANCEL_LABEL": { - "en": "Cancel" + "en": "Cancel", + "de": "Abbruch", + "zh-CN": "取消", + "zh-TW": "取消", + "es": "Cancelar", + "fr": "Annuler", + "it": "Annulla", + "pt": "Cancelar", + "ko-KR": "취소", + "ar": "إلغاء", + "tr": "İptal" }, "FEEDBACK$EMAIL_PLACEHOLDER": { "en": "Enter your email address." }, "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": { - "en": "Initializing agent (may take up to 10 seconds)...", - "zh-CN": "正在初始化智能体(可能需要 10 秒以上时间)", - "de": "Agent wird initialisiert (kann bis zu 10 Sekunden dauern)...", - "ko-KR": "에이전트 설치중(10초 정도 걸립니다)...", - "no": "Initialiserer agent (det kan ta opptil 10 sekunder)...", - "zh-TW": "初始化智能體(可能需要 10 秒以上時間)", - "it": "Inizializzazione dell'agente (può richiedere fino a 10 secondi)...", - "pt": "Inicializando o agente (pode levar até 10 segundos)...", - "es": "Inicializando el agente (puede tardar hasta 10 segundos)...", - "ar": "جاري تهيئة الوكيل (قد يستغرق حتى 10 ثواني)...", - "fr": "Initialisation de l'agent (peut prendre jusqu'à 10 secondes)...", - "tr": "Ajan başlatılıyor (bu işlem 10 saniye kadar sürebilir)..." + "en": "Starting up!", + "de": "Wird gestartet!", + "zh-CN": "正在启动!", + "zh-TW": "正在啟動!", + "ko-KR": "시작 중입니다!", + "no": "Starter opp!", + "it": "Avvio in corso!", + "pt": "Iniciando!", + "es": "¡Iniciando!", + "ar": "جارٍ البدء!", + "fr": "Démarrage en cours !", + "tr": "Başlatılıyor!" }, "CHAT_INTERFACE$AGENT_INIT_MESSAGE": { "en": "Agent is initialized, waiting for task...", "de": "Agent ist initialisiert und wartet auf Aufgabe...", - "zh-CN": "智能体已初始化,等待任务中..." + "zh-CN": "智能体已初始化,等待任务中...", + "zh-TW": "智能體已初始化,等待任務中...", + "ko-KR": "에이전트가 초기화되었습니다. 작업을 기다리는 중...", + "no": "Agenten er initialisert, venter på oppgave...", + "it": "L'agente è inizializzato, in attesa di compiti...", + "pt": "Agente inicializado, aguardando tarefa...", + "es": "El agente está inicializado, esperando tarea...", + "ar": "تم تهيئة الوكيل، في انتظار المهمة...", + "fr": "L'agent est initialisé, en attente de tâche...", + "tr": "Ajan başlatıldı, görev bekleniyor..." }, "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE": { "en": "Agent is running task", "de": "Agent führt Aufgabe aus", - "zh-CN": "智能体正在执行任务..." + "zh-CN": "智能体正在执行任务...", + "zh-TW": "智能體正在執行任務...", + "ko-KR": "에이전트가 작업을 실행 중입니다", + "no": "Agenten utfører oppgave", + "it": "L'agente sta eseguendo il compito", + "pt": "O agente está executando a tarefa", + "es": "El agente está ejecutando la tarea", + "ar": "الوكيل يقوم بتنفيذ المهمة", + "fr": "L'agent exécute la tâche", + "tr": "Ajan görevi yürütüyor" }, "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE": { "en": "Agent is awaiting user input...", "de": "Agent wartet auf Benutzereingabe...", - "zh-CN": "智能体正在等待用户输入..." + "zh-CN": "智能体正在等待用户输入...", + "zh-TW": "智能體正在等待用戶輸入...", + "ko-KR": "에이전트가 사용자 입력을 기다리고 있습니다...", + "no": "Agenten venter på brukerinndata...", + "it": "L'agente è in attesa dell'input dell'utente...", + "pt": "O agente está aguardando a entrada do usuário...", + "es": "El agente está esperando la entrada del usuario...", + "ar": "الوكيل في انتظار إدخال المستخدم...", + "fr": "L'agent attend l'entrée de l'utilisateur...", + "tr": "Ajan kullanıcı girdisini bekliyor..." }, "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": { "en": "Agent has paused.", "de": "Agent pausiert.", - "zh-CN": "智能体已暂停" + "zh-CN": "智能体已暂停", + "zh-TW": "智能體已暫停", + "ko-KR": "에이전트가 일시 중지되었습니다.", + "no": "Agenten har pauset.", + "it": "L'agente ha messo in pausa.", + "pt": "O agente foi pausado.", + "es": "El agente ha pausado.", + "ar": "توقف الوكيل مؤقتًا.", + "fr": "L'agent a mis en pause.", + "tr": "Ajan duraklatıldı." }, "CHAT_INTERFACE$AGENT_STOPPED_MESSAGE": { "en": "Agent has stopped.", "de": "Agent hat angehalten.", - "zh-CN": "智能体已停止" + "zh-CN": "智能体已停止", + "zh-TW": "智能體已停止", + "ko-KR": "에이전트가 중지되었습니다.", + "no": "Agenten har stoppet.", + "it": "L'agente si è fermato.", + "pt": "O agente parou.", + "es": "El agente se ha detenido.", + "ar": "توقف الوكيل.", + "fr": "L'agent s'est arrêté.", + "tr": "Ajan durdu." }, "CHAT_INTERFACE$AGENT_FINISHED_MESSAGE": { "en": "Agent has finished the task.", "de": "Agent hat die Aufgabe erledigt.", - "zh-CN": "智能体已完成任务" + "zh-CN": "智能体已完成任务", + "zh-TW": "智能體已完成任務", + "ko-KR": "에이전트가 작업을 완료했습니다.", + "no": "Agenten har fullført oppgaven.", + "it": "L'agente ha completato il compito.", + "pt": "O agente concluiu a tarefa.", + "es": "El agente ha terminado la tarea.", + "ar": "أنهى الوكيل المهمة.", + "fr": "L'agent a terminé la tâche.", + "tr": "Ajan görevi tamamladı." }, "CHAT_INTERFACE$AGENT_REJECTED_MESSAGE": { "en": "Agent has rejected the task.", "de": "Agent hat die Aufgabe abgelehnt.", - "zh-CN": "智能体拒绝任务" + "zh-CN": "智能体拒绝任务", + "zh-TW": "智能體拒絕任務", + "ko-KR": "에이전트가 작업을 거부했습니다.", + "no": "Agenten har avvist oppgaven.", + "it": "L'agente ha rifiutato il compito.", + "pt": "O agente rejeitou a tarefa.", + "es": "El agente ha rechazado la tarea.", + "ar": "رفض الوكيل المهمة.", + "fr": "L'agent a rejeté la tâche.", + "tr": "Ajan görevi reddetti." }, "CHAT_INTERFACE$AGENT_ERROR_MESSAGE": { "en": "Agent encountered an error.", "de": "Agent ist auf einen Fehler gelaufen.", - "zh-CN": "智能体遇到错误" + "zh-CN": "智能体遇到错误", + "zh-TW": "智能體遇到錯誤", + "ko-KR": "에이전트에 오류가 발생했습니다.", + "no": "Agenten støtte på en feil.", + "it": "L'agente ha riscontrato un errore.", + "pt": "O agente encontrou um erro.", + "es": "El agente encontró un error.", + "ar": "واجه الوكيل خطأ.", + "fr": "L'agent a rencontré une erreur.", + "tr": "Ajan bir hatayla karşılaştı." }, "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": { "en": "Agent is awaiting user confirmation for the pending action.", "de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.", - "zh-CN": "代理正在等待用户确认待处理的操作。" + "zh-CN": "代理正在等待用户确认待处理的操作。", + "zh-TW": "代理正在等待用戶確認待處理的操作。", + "ko-KR": "에이전트가 대기 중인 작업에 대한 사용자 확인을 기다리고 있습니다.", + "no": "Agenten venter på brukerbekreftelse for den ventende handlingen.", + "it": "L'agente è in attesa della conferma dell'utente per l'azione in sospeso.", + "pt": "O agente está aguardando a confirmação do usuário para a ação pendente.", + "es": "El agente está esperando la confirmación del usuario para la acción pendiente.", + "ar": "الوكيل ينتظر تأكيد المستخدم للإجراء المعلق.", + "fr": "L'agent attend la confirmation de l'utilisateur pour l'action en attente.", + "tr": "Ajan, bekleyen işlem için kullanıcı onayını bekliyor." }, "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": { "en": "Agent action has been confirmed!", "de": "Die Aktion des Agenten wurde bestätigt!", - "zh-CN": "代理操作已确认!" + "zh-CN": "代理操作已确认!", + "zh-TW": "代理操作已確認!", + "ko-KR": "에이전트 작업이 확인되었습니다!", + "no": "Agenthandlingen har blitt bekreftet!", + "it": "L'azione dell'agente è stata confermata!", + "pt": "A ação do agente foi confirmada!", + "es": "¡La acción del agente ha sido confirmada!", + "ar": "تم تأكيد إجراء الوكيل!", + "fr": "L'action de l'agent a été confirmée !", + "tr": "Ajan eylemi onaylandı!" }, "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": { "en": "Agent action has been rejected!", "de": "Die Aktion des Agenten wurde abgelehnt!", - "zh-CN": "代理操作已被拒绝!" + "zh-CN": "代理操作已被拒绝!", + "zh-TW": "代理操作已被拒絕!", + "ko-KR": "에이전트 작업이 거부되었습니다!", + "no": "Agenthandlingen har blitt avvist!", + "it": "L'azione dell'agente è stata rifiutata!", + "pt": "A ação do agente foi rejeitada!", + "es": "¡La acción del agente ha sido rechazada!", + "ar": "تم رفض إجراء الوكيل!", + "fr": "L'action de l'agent a été rejetée !", + "tr": "Ajan eylemi reddedildi!" }, "CHAT_INTERFACE$INPUT_PLACEHOLDER": { "en": "Message assistant...", @@ -602,22 +985,58 @@ "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE": { "en": "Continue", "zh-CN": "继续", - "de": "Fortfahren" + "de": "Fortfahren", + "zh-TW": "繼續", + "ko-KR": "계속", + "no": "Fortsett", + "it": "Continua", + "pt": "Continuar", + "es": "Continuar", + "ar": "استمرار", + "fr": "Continuer", + "tr": "Devam et" }, "CHAT_INTERFACE$USER_ASK_CONFIRMATION": { "en": "Do you want to continue with this action?", "de": "Möchten Sie mit dieser Aktion fortfahren?", - "zh-CN": "您要继续此操作吗?" + "zh-CN": "您要继续此操作吗?", + "zh-TW": "您要繼續此操作嗎?", + "ko-KR": "이 작업을 계속하시겠습니까?", + "no": "Vil du fortsette med denne handlingen?", + "it": "Vuoi continuare con questa azione?", + "pt": "Deseja continuar com esta ação?", + "es": "¿Desea continuar con esta acción?", + "ar": "هل تريد الاستمرار في هذا الإجراء؟", + "fr": "Voulez-vous continuer avec cette action ?", + "tr": "Bu işleme devam etmek istiyor musunuz?" }, "CHAT_INTERFACE$USER_CONFIRMED": { "en": "Confirm the requested action", "de": "Bestätigen Sie die angeforderte Aktion", - "zh-CN": "确认请求的操作" + "zh-CN": "确认请求的操作", + "zh-TW": "確認請求的操作", + "ko-KR": "요청된 작업 확인", + "no": "Bekreft den forespurte handlingen", + "it": "Conferma l'azione richiesta", + "pt": "Confirmar a ação solicitada", + "es": "Confirmar la acción solicitada", + "ar": "تأكيد الإجراء المطلوب", + "fr": "Confirmer l'action demandée", + "tr": "İstenen eylemi onayla" }, "CHAT_INTERFACE$USER_REJECTED": { "en": "Reject the requested action", "de": "Lehnen Sie die angeforderte Aktion ab", - "zh-CN": "拒绝请求的操作" + "zh-CN": "拒绝请求的操作", + "zh-TW": "拒絕請求的操作", + "ko-KR": "요청된 작업 거부", + "no": "Avvis den forespurte handlingen", + "it": "Rifiuta l'azione richiesta", + "pt": "Rejeitar a ação solicitada", + "es": "Rechazar la acción solicitada", + "ar": "رفض الإجراء المطلوب", + "fr": "Rejeter l'action demandée", + "tr": "İstenen eylemi reddet" }, "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": { "en": "Send", @@ -635,27 +1054,72 @@ "CHAT_INTERFACE$CHAT_MESSAGE_COPIED": { "en": "Message copied to clipboard", "zh-CN": "消息已复制到剪贴板", - "de": "Nachricht in die Zwischenablage kopiert" + "de": "Nachricht in die Zwischenablage kopiert", + "ko-KR": "메시지가 클립보드에 복사되었습니다", + "no": "Melding kopiert til utklippstavlen", + "zh-TW": "訊息已複製到剪貼簿", + "it": "Messaggio copiato negli appunti", + "pt": "Mensagem copiada para a área de transferência", + "es": "Mensaje copiado al portapapeles", + "ar": "تم نسخ الرسالة إلى الحافظة", + "fr": "Message copié dans le presse-papiers", + "tr": "Mesaj panoya kopyalandı" }, "CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED": { "en": "Failed to copy message to clipboard", "zh-CN": "复制消息到剪贴板失败", - "de": "Nachricht konnte nicht in die Zwischenablage kopiert werden" + "de": "Nachricht konnte nicht in die Zwischenablage kopiert werden", + "ko-KR": "메시지를 클립보드에 복사하지 못했습니다", + "no": "Kunne ikke kopiere meldingen til utklippstavlen", + "zh-TW": "無法將訊息複製到剪貼簿", + "it": "Impossibile copiare il messaggio negli appunti", + "pt": "Falha ao copiar mensagem para a área de transferência", + "es": "No se pudo copiar el mensaje al portapapeles", + "ar": "فشل نسخ الرسالة إلى الحافظة", + "fr": "Échec de la copie du message dans le presse-papiers", + "tr": "Mesaj panoya kopyalanamadı" }, "CHAT_INTERFACE$TOOLTIP_COPY_MESSAGE": { "en": "Copy message", "zh-CN": "复制消息", - "de": "Nachricht kopieren" + "de": "Nachricht kopieren", + "ko-KR": "메시지 복사", + "no": "Kopier melding", + "zh-TW": "複製訊息", + "it": "Copia messaggio", + "pt": "Copiar mensagem", + "es": "Copiar mensaje", + "ar": "نسخ الرسالة", + "fr": "Copier le message", + "tr": "Mesajı kopyala" }, "CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE": { "en": "Send message", "zh-CN": "发送消息", - "de": "Nachricht senden" + "de": "Nachricht senden", + "ko-KR": "메시지 보내기", + "no": "Send melding", + "zh-TW": "發送訊息", + "it": "Invia messaggio", + "pt": "Enviar mensagem", + "es": "Enviar mensaje", + "ar": "إرسال الرسالة", + "fr": "Envoyer le message", + "tr": "Mesaj gönder" }, "CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE": { "en": "Upload image", "zh-CN": "上传图片", - "de": "Bild hochladen" + "de": "Bild hochladen", + "ko-KR": "이미지 업로드", + "no": "Last opp bilde", + "zh-TW": "上傳圖片", + "it": "Carica immagine", + "pt": "Carregar imagem", + "es": "Subir imagen", + "ar": "تحميل الصورة", + "fr": "Télécharger une image", + "tr": "Resim yükle" }, "CHAT_INTERFACE$INITIAL_MESSAGE": { "en": "Hi! I'm OpenHands, an AI Software Engineer. What would you like to build with me today?", @@ -687,7 +1151,16 @@ "CHAT_INTERFACE$TO_BOTTOM": { "en": "To Bottom", "de": "Nach unten", - "zh-CN": "回到底部" + "zh-CN": "回到底部", + "ko-KR": "맨 아래로", + "no": "Til bunnen", + "zh-TW": "回到底部", + "it": "In fondo", + "pt": "Para o fundo", + "es": "Ir al final", + "ar": "إلى الأسفل", + "fr": "Vers le bas", + "tr": "En alta" }, "CHAT_INTERFACE$MESSAGE_ARIA_LABEL": { "en": "Message from {{sender}}", @@ -733,93 +1206,309 @@ "SECURITY_ANALYZER$UNKNOWN_RISK": { "en": "Unknown Risk", "de": "Unbekanntes Risiko", - "zh-CN": "未知风险" + "zh-CN": "未知风险", + "ko-KR": "알 수 없는 위험", + "no": "Ukjent risiko", + "zh-TW": "未知風險", + "it": "Rischio sconosciuto", + "pt": "Risco desconhecido", + "es": "Riesgo desconocido", + "ar": "مخاطر غير معروفة", + "fr": "Risque inconnu", + "tr": "Bilinmeyen risk" }, "SECURITY_ANALYZER$LOW_RISK": { "en": "Low Risk", "de": "Niedriges Risiko", - "zh-CN": "低风险" + "zh-CN": "低风险", + "ko-KR": "낮은 위험", + "no": "Lav risiko", + "zh-TW": "低風險", + "it": "Rischio basso", + "pt": "Baixo risco", + "es": "Riesgo bajo", + "ar": "مخاطر منخفضة", + "fr": "Risque faible", + "tr": "Düşük risk" }, "SECURITY_ANALYZER$MEDIUM_RISK": { "en": "Medium Risk", "de": "Mittleres Risiko", - "zh-CN": "中等风险" + "zh-CN": "中等风险", + "ko-KR": "중간 위험", + "no": "Middels risiko", + "zh-TW": "中等風險", + "it": "Rischio medio", + "pt": "Risco médio", + "es": "Riesgo medio", + "ar": "مخاطر متوسطة", + "fr": "Risque moyen", + "tr": "Orta risk" }, "SECURITY_ANALYZER$HIGH_RISK": { "en": "High Risk", "de": "Hohes Risiko", - "zh-CN": "高风险" + "zh-CN": "高风险", + "ko-KR": "높은 위험", + "no": "Høy risiko", + "zh-TW": "高風險", + "it": "Rischio elevato", + "pt": "Alto risco", + "es": "Riesgo alto", + "ar": "مخاطر عالية", + "fr": "Risque élevé", + "tr": "Yüksek risk" }, "SETTINGS$MODEL_TOOLTIP": { "en": "Select the language model to use.", "zh-CN": "选择要使用的语言模型", "zh-TW": "選擇要使用的語言模型。", - "de": "Wähle das zu verwendende Modell." + "de": "Wähle das zu verwendende Modell.", + "ko-KR": "사용할 언어 모델을 선택하세요.", + "no": "Velg språkmodellen som skal brukes.", + "it": "Seleziona il modello linguistico da utilizzare.", + "pt": "Selecione o modelo de linguagem a ser usado.", + "es": "Seleccione el modelo de lenguaje a utilizar.", + "ar": "اختر نموذج اللغة المراد استخدامه.", + "fr": "Sélectionnez le modèle de langage à utiliser.", + "tr": "Kullanılacak dil modelini seçin." }, "SETTINGS$AGENT_TOOLTIP": { "en": "Select the agent to use.", "zh-CN": "选择要使用的智能体", "zh-TW": "選擇要使用的智能體。", - "de": "Wähle den zu verwendenden Agenten." + "de": "Wähle den zu verwendenden Agenten.", + "ko-KR": "사용할 에이전트를 선택하세요.", + "no": "Velg agenten som skal brukes.", + "it": "Seleziona l'agente da utilizzare.", + "pt": "Selecione o agente a ser usado.", + "es": "Seleccione el agente a utilizar.", + "ar": "اختر الوكيل المراد استخدامه.", + "fr": "Sélectionnez l'agent à utiliser.", + "tr": "Kullanılacak ajanı seçin." }, "SETTINGS$LANGUAGE_TOOLTIP": { "en": "Select the language for the UI.", "zh-CN": "选择界面语言", "zh-TW": "選擇 UI 的語言。", - "de": "Wähle die Sprache für die Oberfläche." + "de": "Wähle die Sprache für die Oberfläche.", + "ko-KR": "UI 언어를 선택하세요.", + "no": "Velg språk for brukergrensesnittet.", + "it": "Seleziona la lingua per l'interfaccia utente.", + "pt": "Selecione o idioma para a interface do usuário.", + "es": "Seleccione el idioma para la interfaz de usuario.", + "ar": "اختر لغة واجهة المستخدم.", + "fr": "Sélectionnez la langue de l'interface utilisateur.", + "tr": "Kullanıcı arayüzü için dil seçin." }, "SETTINGS$DISABLED_RUNNING": { "en": "Cannot be changed while the agent is running.", "zh-CN": "在智能体运行时无法更改", "zh-TW": "智能體正在執行時無法更改。", - "de": "Kann bei laufender Aufgabe nicht geändert werden." + "de": "Kann bei laufender Aufgabe nicht geändert werden.", + "ko-KR": "에이전트가 실행 중일 때는 변경할 수 없습니다.", + "no": "Kan ikke endres mens agenten kjører.", + "it": "Non può essere modificato mentre l'agente è in esecuzione.", + "pt": "Não pode ser alterado enquanto o agente está em execução.", + "es": "No se puede cambiar mientras el agente está en ejecución.", + "ar": "لا يمكن تغييره أثناء تشغيل الوكيل.", + "fr": "Ne peut pas être modifié pendant que l'agent est en cours d'exécution.", + "tr": "Ajan çalışırken değiştirilemez." }, "SETTINGS$API_KEY_PLACEHOLDER": { "en": "Enter your API key.", "zh-CN": "输入您的 API key", "zh-TW": "輸入您的 API 金鑰。", - "de": "Modell API Schlüssel." + "de": "Modell API Schlüssel.", + "ko-KR": "API 키를 입력하세요.", + "no": "Skriv inn din API-nøkkel.", + "it": "Inserisci la tua chiave API.", + "pt": "Digite sua chave de API.", + "es": "Ingrese su clave de API.", + "ar": "أدخل مفتاح API الخاص بك.", + "fr": "Entrez votre clé API.", + "tr": "API anahtarınızı girin." }, "SETTINGS$CONFIRMATION_MODE": { "en": "Enable Confirmation Mode", "de": "Bestätigungsmodus aktivieren", - "zh-CN": "启用确认模式" + "zh-CN": "启用确认模式", + "zh-TW": "啟用確認模式", + "ko-KR": "확인 모드 활성화", + "no": "Aktiver bekreftelsesmodus", + "it": "Abilita modalità di conferma", + "pt": "Ativar modo de confirmação", + "es": "Habilitar modo de confirmación", + "ar": "تفعيل وضع التأكيد", + "fr": "Activer le mode de confirmation", + "tr": "Onay Modunu Etkinleştir" }, "SETTINGS$CONFIRMATION_MODE_TOOLTIP": { "en": "Awaits for user confirmation before executing code.", "de": "Wartet auf die Bestätigung des Benutzers, bevor der Code ausgeführt wird.", - "zh-CN": "在执行代码之前等待用户确认。" + "zh-CN": "在执行代码之前等待用户确认。", + "zh-TW": "在執行程式碼之前等待使用者確認。", + "ko-KR": "코드 실행 전 사용자 확인을 기다립니다.", + "no": "Venter på brukerbekreftelse før koden utføres.", + "it": "Attende la conferma dell'utente prima di eseguire il codice.", + "pt": "Aguarda a confirmação do usuário antes de executar o código.", + "es": "Espera la confirmación del usuario antes de ejecutar el código.", + "ar": "ينتظر تأكيد المستخدم قبل تنفيذ الكود.", + "fr": "Attend la confirmation de l'utilisateur avant d'exécuter le code.", + "tr": "Kodu çalıştırmadan önce kullanıcı onayını bekler." }, "SETTINGS$AGENT_SELECT_ENABLED": { - "en": "Enable Agent Selection - Advanced Users" + "en": "Enable Agent Selection - Advanced Users", + "zh-CN": "启用智能体选择 - 高级用户", + "zh-TW": "啟用智能體選擇 - 進階使用者", + "de": "Agentenauswahl aktivieren - Fortgeschrittene Benutzer", + "ko-KR": "에이전트 선택 활성화 - 고급 사용자", + "no": "Aktiver agentvalg - Avanserte brukere", + "it": "Abilita selezione agente - Utenti avanzati", + "pt": "Ativar seleção de agente - Usuários avançados", + "es": "Habilitar selección de agente - Usuarios avanzados", + "ar": "تمكين اختيار الوكيل - المستخدمين المتقدمين", + "fr": "Activer la sélection d'agent - Utilisateurs avancés", + "tr": "Ajan Seçimini Etkinleştir - İleri Düzey Kullanıcılar" }, "SETTINGS$SECURITY_ANALYZER": { "en": "Enable Security Analyzer", "de": "Sicherheitsanalysator aktivieren", - "zh-CN": "启用安全分析器" + "zh-CN": "启用安全分析器", + "zh-TW": "啟用安全分析器", + "ko-KR": "보안 분석기 활성화", + "no": "Aktiver sikkerhetsanalysator", + "it": "Abilita analizzatore di sicurezza", + "pt": "Ativar analisador de segurança", + "es": "Habilitar analizador de seguridad", + "ar": "تمكين محلل الأمان", + "fr": "Activer l'analyseur de sécurité", + "tr": "Güvenlik Analizörünü Etkinleştir" }, "BROWSER$EMPTY_MESSAGE": { "en": "No page loaded.", "zh-CN": "页面未加载", "zh-TW": "未加載任何頁面。", - "de": "Keine Seite geladen." + "de": "Keine Seite geladen.", + "ko-KR": "페이지가 로드되지 않았습니다.", + "no": "Ingen side lastet.", + "it": "Nessuna pagina caricata.", + "pt": "Nenhuma página carregada.", + "es": "Ninguna página cargada.", + "ar": "لم يتم تحميل أي صفحة.", + "fr": "Aucune page chargée.", + "tr": "Sayfa yüklenmedi." }, "PLANNER$EMPTY_MESSAGE": { "en": "No plan created.", "zh-CN": "计划未创建", "zh-TW": "未創建任何計劃。", - "de": "Kein Plan erstellt." + "de": "Kein Plan erstellt.", + "ko-KR": "생성된 계획이 없습니다.", + "no": "Ingen plan opprettet.", + "it": "Nessun piano creato.", + "pt": "Nenhum plano criado.", + "es": "Ningún plan creado.", + "ar": "لم يتم إنشاء أي خطة.", + "fr": "Aucun plan créé.", + "tr": "Plan oluşturulmadı." }, "FEEDBACK$PUBLIC_LABEL": { "en": "Public", "zh-CN": "公开", - "zh-TW": "公開。", - "de": "Öffentlich" + "zh-TW": "公開", + "de": "Öffentlich", + "ko-KR": "공개", + "no": "Offentlig", + "it": "Pubblico", + "pt": "Público", + "es": "Público", + "ar": "عام", + "fr": "Public", + "tr": "Herkese Açık" }, "FEEDBACK$PRIVATE_LABEL": { "en": "Private", "zh-CN": "私有", - "zh-TW": "私有。", - "de": "Privat" + "zh-TW": "私有", + "de": "Privat", + "ko-KR": "비공개", + "no": "Privat", + "it": "Privato", + "pt": "Privado", + "es": "Privado", + "ar": "خاص", + "fr": "Privé", + "tr": "Özel" + }, + "STATUS$STARTING_RUNTIME": { + "en": "Starting Runtime...", + "zh-CN": "启动运行时...", + "zh-TW": "啟動運行時...", + "de": "Laufzeitumgebung wird gestartet...", + "ko-KR": "런타임 시작 중...", + "no": "Starter kjøretidsmiljø...", + "it": "Avvio dell'ambiente di esecuzione...", + "pt": "Iniciando o ambiente de execução...", + "es": "Iniciando el entorno de ejecución...", + "ar": "جارٍ بدء بيئة التشغيل...", + "fr": "Démarrage de l'environnement d'exécution...", + "tr": "Çalışma zamanı başlatılıyor..." + }, + "STATUS$STARTING_CONTAINER": { + "en": "Preparing container, this might take a few minutes...", + "zh-CN": "正在准备容器,这可能需要几分钟...", + "zh-TW": "正在準備容器,這可能需要幾分鐘...", + "de": "Container wird vorbereitet, dies kann einige Minuten dauern...", + "ko-KR": "컨테이너를 준비 중입니다. 몇 분 정도 걸릴 수 있습니다...", + "no": "Forbereder container, dette kan ta noen minutter...", + "it": "Preparazione del container in corso, potrebbe richiedere alcuni minuti...", + "pt": "Preparando o container, isso pode levar alguns minutos...", + "es": "Preparando el contenedor, esto puede tardar unos minutos...", + "ar": "جارٍ إعداد الحاوية، قد يستغرق هذا بضع دقائق...", + "fr": "Préparation du conteneur, cela peut prendre quelques minutes...", + "tr": "Konteyner hazırlanıyor, bu işlem birkaç dakika sürebilir..." + }, + "STATUS$PREPARING_CONTAINER": { + "en": "Preparing to start container...", + "zh-CN": "正在准备启动容器...", + "zh-TW": "正在準備啟動容器...", + "de": "Vorbereitung zum Starten des Containers...", + "ko-KR": "컨테이너 시작 준비 중...", + "no": "Forbereder å starte container...", + "it": "Preparazione all'avvio del container...", + "pt": "Preparando para iniciar o container...", + "es": "Preparando para iniciar el contenedor...", + "ar": "جارٍ التحضير لبدء الحاوية...", + "fr": "Préparation du démarrage du conteneur...", + "tr": "Konteyner başlatılmaya hazırlanıyor..." + }, + "STATUS$CONTAINER_STARTED": { + "en": "Container started.", + "zh-CN": "容器已启动。", + "zh-TW": "容器已啟動。", + "de": "Container gestartet.", + "ko-KR": "컨테이너가 시작되었습니다.", + "no": "Container startet.", + "it": "Container avviato.", + "pt": "Container iniciado.", + "es": "Contenedor iniciado.", + "ar": "تم بدء الحاوية.", + "fr": "Conteneur démarré.", + "tr": "Konteyner başlatıldı." + }, + "STATUS$WAITING_FOR_CLIENT": { + "en": "Waiting for client to become ready...", + "zh-CN": "等待客户端准备就绪...", + "zh-TW": "等待客戶端準備就緒...", + "de": "Warten auf Bereitschaft des Clients...", + "ko-KR": "클라이언트가 준비될 때까지 기다리는 중...", + "no": "Venter på at klienten skal bli klar...", + "it": "In attesa che il client sia pronto...", + "pt": "Aguardando o cliente ficar pronto...", + "es": "Esperando a que el cliente esté listo...", + "ar": "في انتظار جاهزية العميل...", + "fr": "En attente que le client soit prêt...", + "tr": "İstemcinin hazır olması bekleniyor..." } } diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts index 72b1a4a8cf..1f2e99a379 100644 --- a/frontend/src/services/actions.ts +++ b/frontend/src/services/actions.ts @@ -6,10 +6,11 @@ import { ActionSecurityRisk, appendSecurityAnalyzerInput, } from "#/state/securityAnalyzerSlice"; +import { setCurStatusMessage } from "#/state/statusSlice"; import { setRootTask } from "#/state/taskSlice"; import store from "#/store"; import ActionType from "#/types/ActionType"; -import { ActionMessage } from "#/types/Message"; +import { ActionMessage, StatusMessage } from "#/types/Message"; import { SocketMessage } from "#/types/ResponseType"; import { handleObservationMessage } from "./observations"; import { getRootTask } from "./taskService"; @@ -138,6 +139,16 @@ export function handleActionMessage(message: ActionMessage) { } } +export function handleStatusMessage(message: StatusMessage) { + const msg = message.message == null ? "" : message.message.trim(); + store.dispatch( + setCurStatusMessage({ + ...message, + message: msg, + }), + ); +} + export function handleAssistantMessage(data: string | SocketMessage) { let socketMessage: SocketMessage; @@ -149,7 +160,9 @@ export function handleAssistantMessage(data: string | SocketMessage) { if ("action" in socketMessage) { handleActionMessage(socketMessage); - } else { + } else if ("observation" in socketMessage) { handleObservationMessage(socketMessage); + } else if ("message" in socketMessage) { + handleStatusMessage(socketMessage); } } diff --git a/frontend/src/services/session.ts b/frontend/src/services/session.ts index 392905eaa3..8e77a33cf2 100644 --- a/frontend/src/services/session.ts +++ b/frontend/src/services/session.ts @@ -8,11 +8,19 @@ import { I18nKey } from "#/i18n/declaration"; const translate = (key: I18nKey) => i18next.t(key); +// Define a type for the messages +type Message = { + action: ActionType; + args: Record; +}; + class Session { private static _socket: WebSocket | null = null; private static _latest_event_id: number = -1; + private static _messageQueue: Message[] = []; + public static _history: Record[] = []; // callbacks contain a list of callable functions @@ -83,6 +91,7 @@ class Session { toast.success("ws", translate(I18nKey.SESSION$SERVER_CONNECTED_MESSAGE)); Session._connecting = false; Session._initializeAgent(); + Session._flushQueue(); Session.callbacks.open?.forEach((callback) => { callback(e); }); @@ -94,7 +103,6 @@ class Session { data = JSON.parse(e.data); Session._history.push(data); } catch (err) { - // TODO: report the error toast.error( "ws", translate(I18nKey.SESSION$SESSION_HANDLING_ERROR_MESSAGE), @@ -115,6 +123,7 @@ class Session { }; Session._socket.onerror = () => { + // TODO report error toast.error( "ws", translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE), @@ -145,9 +154,20 @@ class Session { Session._socket = null; } + private static _flushQueue(): void { + while (Session._messageQueue.length > 0) { + const message = Session._messageQueue.shift(); + if (message) { + setTimeout(() => Session.send(JSON.stringify(message)), 1000); + } + } + } + static send(message: string): void { + const messageObject: Message = JSON.parse(message); + if (Session._connecting) { - setTimeout(() => Session.send(message), 1000); + Session._messageQueue.push(messageObject); return; } if (!Session.isConnected()) { diff --git a/frontend/src/state/statusSlice.ts b/frontend/src/state/statusSlice.ts new file mode 100644 index 0000000000..5517d6af86 --- /dev/null +++ b/frontend/src/state/statusSlice.ts @@ -0,0 +1,23 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { StatusMessage } from "#/types/Message"; + +const initialStatusMessage: StatusMessage = { + message: "", + is_error: false, +}; + +export const statusSlice = createSlice({ + name: "status", + initialState: { + curStatusMessage: initialStatusMessage, + }, + reducers: { + setCurStatusMessage: (state, action: PayloadAction) => { + state.curStatusMessage = action.payload; + }, + }, +}); + +export const { setCurStatusMessage } = statusSlice.actions; + +export default statusSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 7fffbfb570..0de8d08d07 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -8,6 +8,7 @@ import errorsReducer from "./state/errorsSlice"; import taskReducer from "./state/taskSlice"; import jupyterReducer from "./state/jupyterSlice"; import securityAnalyzerReducer from "./state/securityAnalyzerSlice"; +import statusReducer from "./state/statusSlice"; export const rootReducer = combineReducers({ browser: browserReducer, @@ -19,6 +20,7 @@ export const rootReducer = combineReducers({ agent: agentReducer, jupyter: jupyterReducer, securityAnalyzer: securityAnalyzerReducer, + status: statusReducer, }); const store = configureStore({ diff --git a/frontend/src/types/Message.tsx b/frontend/src/types/Message.tsx index 515441c74c..a7a062cd6e 100644 --- a/frontend/src/types/Message.tsx +++ b/frontend/src/types/Message.tsx @@ -31,3 +31,12 @@ export interface ObservationMessage { // The timestamp of the message timestamp: string; } + +export interface StatusMessage { + // TODO not implemented yet + // Whether the status is an error, default is false + is_error: boolean; + + // A status message to display to the user + message: string; +} diff --git a/frontend/src/types/ResponseType.tsx b/frontend/src/types/ResponseType.tsx index b635d78c33..cad6131f80 100644 --- a/frontend/src/types/ResponseType.tsx +++ b/frontend/src/types/ResponseType.tsx @@ -1,5 +1,5 @@ -import { ActionMessage, ObservationMessage } from "./Message"; +import { ActionMessage, ObservationMessage, StatusMessage } from "./Message"; -type SocketMessage = ActionMessage | ObservationMessage; +type SocketMessage = ActionMessage | ObservationMessage | StatusMessage; export { type SocketMessage }; diff --git a/openhands/core/main.py b/openhands/core/main.py index c25ba9a0d8..3aa6b5ef18 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -55,7 +55,6 @@ def create_runtime( config: The app config. sid: The session id. - runtime_tools_config: (will be deprecated) The runtime tools config. """ # if sid is provided on the command line, use it as the name of the event stream # otherwise generate it on the basis of the configured jwt_secret diff --git a/openhands/runtime/client/client.py b/openhands/runtime/client/client.py index 987fddf909..1b34eb5b53 100644 --- a/openhands/runtime/client/client.py +++ b/openhands/runtime/client/client.py @@ -16,8 +16,10 @@ from pathlib import Path import pexpect from fastapi import FastAPI, HTTPException, Request, UploadFile +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel +from starlette.exceptions import HTTPException as StarletteHTTPException from uvicorn import run from openhands.core.logger import openhands_logger as logger @@ -562,6 +564,35 @@ if __name__ == '__main__': app = FastAPI(lifespan=lifespan) + # TODO below 3 exception handlers were recommended by Sonnet. + # Are these something we should keep? + @app.exception_handler(Exception) + async def global_exception_handler(request: Request, exc: Exception): + logger.exception('Unhandled exception occurred:') + return JSONResponse( + status_code=500, + content={ + 'message': 'An unexpected error occurred. Please try again later.' + }, + ) + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request: Request, exc: StarletteHTTPException): + logger.error(f'HTTP exception occurred: {exc.detail}') + return JSONResponse( + status_code=exc.status_code, content={'message': exc.detail} + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler( + request: Request, exc: RequestValidationError + ): + logger.error(f'Validation error occurred: {exc}') + return JSONResponse( + status_code=422, + content={'message': 'Invalid request parameters', 'details': exc.errors()}, + ) + @app.middleware('http') async def one_request_at_a_time(request: Request, call_next): assert client is not None diff --git a/openhands/runtime/client/runtime.py b/openhands/runtime/client/runtime.py index 6a8d5eea3e..bb2c78b79a 100644 --- a/openhands/runtime/client/runtime.py +++ b/openhands/runtime/client/runtime.py @@ -2,6 +2,7 @@ import os import tempfile import threading import uuid +from typing import Callable from zipfile import ZipFile import docker @@ -119,6 +120,7 @@ class EventStreamRuntime(Runtime): sid: str = 'default', plugins: list[PluginRequirement] | None = None, env_vars: dict[str, str] | None = None, + status_message_callback: Callable | None = None, ): self.config = config self._host_port = 30000 # initial dummy value @@ -130,12 +132,13 @@ class EventStreamRuntime(Runtime): self.instance_id = ( sid + '_' + str(uuid.uuid4()) if sid is not None else str(uuid.uuid4()) ) + self.status_message_callback = status_message_callback + self.send_status_message('STATUS$STARTING_RUNTIME') self.docker_client: docker.DockerClient = self._init_docker_client() self.base_container_image = self.config.sandbox.base_container_image self.runtime_container_image = self.config.sandbox.runtime_container_image self.container_name = self.container_name_prefix + self.instance_id - self.container = None self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time @@ -146,9 +149,10 @@ class EventStreamRuntime(Runtime): self.log_buffer: LogBuffer | None = None if self.config.sandbox.runtime_extra_deps: - logger.info( + logger.debug( f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}' ) + self.skip_container_logs = ( os.environ.get('SKIP_CONTAINER_LOGS', 'false').lower() == 'true' ) @@ -157,6 +161,8 @@ class EventStreamRuntime(Runtime): raise ValueError( 'Neither runtime container image nor base container image is set' ) + logger.info('Preparing container, this might take a few minutes...') + self.send_status_message('STATUS$STARTING_CONTAINER') self.runtime_container_image = build_runtime_image( self.base_container_image, self.runtime_builder, @@ -169,9 +175,13 @@ class EventStreamRuntime(Runtime): ) # will initialize both the event stream and the env vars - super().__init__(config, event_stream, sid, plugins, env_vars) + super().__init__( + config, event_stream, sid, plugins, env_vars, status_message_callback + ) + + logger.info('Waiting for client to become ready...') + self.send_status_message('STATUS$WAITING_FOR_CLIENT') - logger.info('Waiting for runtime container to be alive...') self._wait_until_alive() self.setup_initial_env() @@ -179,6 +189,7 @@ class EventStreamRuntime(Runtime): logger.info( f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}' ) + self.send_status_message(' ') @staticmethod def _init_docker_client() -> docker.DockerClient: @@ -201,9 +212,8 @@ class EventStreamRuntime(Runtime): plugins: list[PluginRequirement] | None = None, ): try: - logger.info( - f'Starting container with image: {self.runtime_container_image} and name: {self.container_name}' - ) + logger.info('Preparing to start container...') + self.send_status_message('STATUS$PREPARING_CONTAINER') plugin_arg = '' if plugins is not None and len(plugins) > 0: plugin_arg = ( @@ -241,17 +251,17 @@ class EventStreamRuntime(Runtime): if self.config.debug: environment['DEBUG'] = 'true' - logger.info(f'Workspace Base: {self.config.workspace_base}') + logger.debug(f'Workspace Base: {self.config.workspace_base}') if mount_dir is not None and sandbox_workspace_dir is not None: # e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}} volumes = {mount_dir: {'bind': sandbox_workspace_dir, 'mode': 'rw'}} - logger.info(f'Mount dir: {mount_dir}') + logger.debug(f'Mount dir: {mount_dir}') else: logger.warn( 'Warning: Mount dir is not set, will not mount the workspace directory to the container!\n' ) volumes = None - logger.info(f'Sandbox workspace: {sandbox_workspace_dir}') + logger.debug(f'Sandbox workspace: {sandbox_workspace_dir}') if self.config.sandbox.browsergym_eval_env is not None: browsergym_arg = ( @@ -259,6 +269,7 @@ class EventStreamRuntime(Runtime): ) else: browsergym_arg = '' + container = self.docker_client.containers.run( self.runtime_container_image, command=( @@ -281,6 +292,7 @@ class EventStreamRuntime(Runtime): ) self.log_buffer = LogBuffer(container) logger.info(f'Container started. Server url: {self.api_url}') + self.send_status_message('STATUS$CONTAINER_STARTED') return container except Exception as e: logger.error( @@ -539,3 +551,8 @@ class EventStreamRuntime(Runtime): return port # If no port is found after max_attempts, return the last tried port return port + + def send_status_message(self, message: str): + """Sends a status message if the callback function was provided.""" + if self.status_message_callback: + self.status_message_callback(message) diff --git a/openhands/runtime/e2b/runtime.py b/openhands/runtime/e2b/runtime.py index 82ca16f939..d2988895ba 100644 --- a/openhands/runtime/e2b/runtime.py +++ b/openhands/runtime/e2b/runtime.py @@ -1,3 +1,5 @@ +from typing import Callable, Optional + from openhands.core.config import AppConfig from openhands.events.action import ( FileReadAction, @@ -25,8 +27,15 @@ class E2BRuntime(Runtime): sid: str = 'default', plugins: list[PluginRequirement] | None = None, sandbox: E2BSandbox | None = None, + status_message_callback: Optional[Callable] = None, ): - super().__init__(config, event_stream, sid, plugins) + super().__init__( + config, + event_stream, + sid, + plugins, + status_message_callback=status_message_callback, + ) if sandbox is None: self.sandbox = E2BSandbox() if not isinstance(self.sandbox, E2BSandbox): diff --git a/openhands/runtime/remote/runtime.py b/openhands/runtime/remote/runtime.py index d37433c3d4..9cc0ebe7ba 100644 --- a/openhands/runtime/remote/runtime.py +++ b/openhands/runtime/remote/runtime.py @@ -2,6 +2,7 @@ import os import tempfile import threading import uuid +from typing import Callable, Optional from zipfile import ZipFile import requests @@ -55,6 +56,7 @@ class RemoteRuntime(Runtime): sid: str = 'default', plugins: list[PluginRequirement] | None = None, env_vars: dict[str, str] | None = None, + status_message_callback: Optional[Callable] = None, ): self.config = config if self.config.sandbox.api_hostname == 'localhost': @@ -168,7 +170,9 @@ class RemoteRuntime(Runtime): ) # Initialize the eventstream and env vars - super().__init__(config, event_stream, sid, plugins, env_vars) + super().__init__( + config, event_stream, sid, plugins, env_vars, status_message_callback + ) logger.info( f'Runtime initialized with plugins: {[plugin.name for plugin in self.plugins]}' diff --git a/openhands/runtime/runtime.py b/openhands/runtime/runtime.py index 902e6027f2..9c7fbe5447 100644 --- a/openhands/runtime/runtime.py +++ b/openhands/runtime/runtime.py @@ -3,6 +3,7 @@ import copy import json import os from abc import abstractmethod +from typing import Callable from openhands.core.config import AppConfig, SandboxConfig from openhands.core.logger import openhands_logger as logger @@ -58,11 +59,13 @@ class Runtime: sid: str = 'default', plugins: list[PluginRequirement] | None = None, env_vars: dict[str, str] | None = None, + status_message_callback: Callable | None = None, ): self.sid = sid self.event_stream = event_stream self.event_stream.subscribe(EventStreamSubscriber.RUNTIME, self.on_event) self.plugins = plugins if plugins is not None and len(plugins) > 0 else [] + self.status_message_callback = status_message_callback self.config = copy.deepcopy(config) atexit.register(self.close) diff --git a/openhands/server/session/agent.py b/openhands/server/session/agent_session.py similarity index 86% rename from openhands/server/session/agent.py rename to openhands/server/session/agent_session.py index 31a8a821cb..bb55d37b2f 100644 --- a/openhands/server/session/agent.py +++ b/openhands/server/session/agent_session.py @@ -1,3 +1,6 @@ +import asyncio +from typing import Callable, Optional + from openhands.controller import AgentController from openhands.controller.agent import Agent from openhands.controller.state.state import State @@ -46,9 +49,9 @@ class AgentSession: max_budget_per_task: float | None = None, agent_to_llm_config: dict[str, LLMConfig] | None = None, agent_configs: dict[str, AgentConfig] | None = None, + status_message_callback: Optional[Callable] = None, ): """Starts the Agent session - Parameters: - runtime_name: The name of the runtime associated with the session - config: @@ -58,13 +61,12 @@ class AgentSession: - agent_to_llm_config: - agent_configs: """ - if self.controller or self.runtime: raise RuntimeError( 'Session already started. You need to close this session and start a new one.' ) await self._create_security_analyzer(config.security.security_analyzer) - await self._create_runtime(runtime_name, config, agent) + await self._create_runtime(runtime_name, config, agent, status_message_callback) await self._create_controller( agent, config.security.confirmation_mode, @@ -96,13 +98,19 @@ class AgentSession: - security_analyzer: The name of the security analyzer to use """ - logger.info(f'Using security analyzer: {security_analyzer}') if security_analyzer: + logger.debug(f'Using security analyzer: {security_analyzer}') self.security_analyzer = options.SecurityAnalyzers.get( security_analyzer, SecurityAnalyzer )(self.event_stream) - async def _create_runtime(self, runtime_name: str, config: AppConfig, agent: Agent): + async def _create_runtime( + self, + runtime_name: str, + config: AppConfig, + agent: Agent, + status_message_callback: Optional[Callable] = None, + ): """Creates a runtime instance Parameters: @@ -112,17 +120,27 @@ class AgentSession: """ if self.runtime is not None: - raise Exception('Runtime already created') + raise RuntimeError('Runtime already created') logger.info(f'Initializing runtime `{runtime_name}` now...') runtime_cls = get_runtime_cls(runtime_name) - self.runtime = runtime_cls( + + self.runtime = await asyncio.to_thread( + runtime_cls, config=config, event_stream=self.event_stream, sid=self.sid, plugins=agent.sandbox_plugins, + status_message_callback=status_message_callback, ) + if self.runtime is not None: + logger.debug( + f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}' + ) + else: + logger.warning('Runtime initialization failed') + async def _create_controller( self, agent: Agent, @@ -178,5 +196,5 @@ class AgentSession: ) logger.info(f'Restored agent state from session, sid: {self.sid}') except Exception as e: - logger.info(f'Error restoring state: {e}') + logger.info(f'State could not be restored: {e}') logger.info('Agent controller initialized.') diff --git a/openhands/server/session/manager.py b/openhands/server/session/manager.py index a14fdc8be1..99da3bc4cb 100644 --- a/openhands/server/session/manager.py +++ b/openhands/server/session/manager.py @@ -35,9 +35,11 @@ class SessionManager: async def send(self, sid: str, data: dict[str, object]) -> bool: """Sends data to the client.""" - if sid not in self._sessions: + session = self.get_session(sid) + if session is None: + logger.error(f'*** No session found for {sid}, skipping message ***') return False - return await self._sessions[sid].send(data) + return await session.send(data) async def send_error(self, sid: str, message: str) -> bool: """Sends an error message to the client.""" diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index 588df19610..fd9e9aa578 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -21,7 +21,7 @@ from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.stream import EventStreamSubscriber from openhands.llm.llm import LLM from openhands.runtime.utils.shutdown_listener import should_continue -from openhands.server.session.agent import AgentSession +from openhands.server.session.agent_session import AgentSession from openhands.storage.files import FileStore DEL_DELT_SEC = 60 * 60 * 5 @@ -33,6 +33,7 @@ class Session: last_active_ts: int = 0 is_alive: bool = True agent_session: AgentSession + loop: asyncio.AbstractEventLoop def __init__( self, sid: str, ws: WebSocket | None, config: AppConfig, file_store: FileStore @@ -45,6 +46,7 @@ class Session: EventStreamSubscriber.SERVER, self.on_event ) self.config = config + self.loop = asyncio.get_event_loop() async def close(self): self.is_alive = False @@ -113,6 +115,7 @@ class Session: max_budget_per_task=self.config.max_budget_per_task, agent_to_llm_config=self.config.get_agent_to_llm_config_map(), agent_configs=self.config.get_agent_configs(), + status_message_callback=self.queue_status_message, ) except Exception as e: logger.exception(f'Error creating controller: {e}') @@ -125,7 +128,8 @@ class Session: ) async def on_event(self, event: Event): - """Callback function for agent events. + """Callback function for events that mainly come from the agent. + Event is the base class for any agent action and observation. Args: event: The agent event (Observation or Action). @@ -135,7 +139,6 @@ class Session: if isinstance(event, NullObservation): return if event.source == EventSource.AGENT: - logger.info('Server event') await self.send(event_to_dict(event)) elif event.source == EventSource.USER and isinstance( event, CmdOutputObservation @@ -172,6 +175,9 @@ class Session: await asyncio.sleep(0.001) # This flushes the data to the client self.last_active_ts = int(time.time()) return True + except RuntimeError: + self.is_alive = False + return False except WebSocketDisconnect: self.is_alive = False return False @@ -195,3 +201,8 @@ class Session: return False self.is_alive = data.get('is_alive', False) return True + + def queue_status_message(self, message: str): + """Queues a status message to be sent asynchronously.""" + # Ensure the coroutine runs in the main event loop + asyncio.run_coroutine_threadsafe(self.send_message(message), self.loop)