From 83b7fcc3ed246a8e1b669ffb01f7ee7e2537e1fe Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:49:39 +0400 Subject: [PATCH 01/12] hotfix(frontend): Clear base URL when advanced options is unselected (#4016) --- frontend/src/components/modals/settings/SettingsForm.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/modals/settings/SettingsForm.tsx b/frontend/src/components/modals/settings/SettingsForm.tsx index e2ea5ed0577..aa1fa95d552 100644 --- a/frontend/src/components/modals/settings/SettingsForm.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.tsx @@ -52,13 +52,19 @@ function SettingsForm({ const [enableAdvanced, setEnableAdvanced] = React.useState(advancedAlreadyInUse); + const handleAdvancedChange = (value: boolean) => { + setEnableAdvanced(value); + // Reset the base URL if the user disables advanced options + if (!value) onBaseURLChange(""); + }; + return ( <> setEnableAdvanced(value)} + onValueChange={handleAdvancedChange} > Advanced Options From 582f07f9c93baf410f8351c0e902e901a8cbdacb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:51:39 +0400 Subject: [PATCH 02/12] chore(deps-dev): bump @types/node from 22.5.5 to 22.6.1 in /frontend (#4028) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1be8ba8ae19..8736a1ca2cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.5.5", + "@types/node": "^22.6.1", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", "@types/react-highlight": "^0.12.8", @@ -4860,9 +4860,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "22.5.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", - "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", "devOptional": true, "dependencies": { "undici-types": "~6.19.2" diff --git a/frontend/package.json b/frontend/package.json index 9b9b8883d17..339da8da268 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,7 +64,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.5.5", + "@types/node": "^22.6.1", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", "@types/react-highlight": "^0.12.8", From a66e73895733b8ec592b185180c1a97d69375ecc Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Tue, 24 Sep 2024 10:59:06 -0500 Subject: [PATCH 03/12] [eval] use mp Pool instead ProcessPoolExecutor (#4025) --- evaluation/utils/shared.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py index ed476580cb4..d3850882882 100644 --- a/evaluation/utils/shared.py +++ b/evaluation/utils/shared.py @@ -6,7 +6,6 @@ import subprocess import time import traceback -from concurrent.futures import ProcessPoolExecutor, as_completed from typing import Any, Awaitable, Callable, TextIO import pandas as pd @@ -328,21 +327,22 @@ def run_evaluation( try: if use_multiprocessing: - with ProcessPoolExecutor(num_workers) as executor: - futures = [ - executor.submit( + with mp.Pool(num_workers) as pool: + results = [ + pool.apply_async( _process_instance_wrapper, - process_instance_func=process_instance_func, - instance=instance, - metadata=metadata, - use_mp=True, - max_retries=max_retries, + args=( + process_instance_func, + instance, + metadata, + True, + max_retries, + ), ) for _, instance in dataset.iterrows() ] - for future in as_completed(futures): - result = future.result() - update_progress(result, pbar, output_fp) + for result in results: + update_progress(result.get(), pbar, output_fp) else: for _, instance in dataset.iterrows(): result = _process_instance_wrapper( From 5d77aec90bd2c916b2e5aaec527896a9d5465853 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:05:50 +0200 Subject: [PATCH 04/12] chore(deps): bump litellm from 1.47.1 to 1.48.0 (#4032) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index df69f9064f7..dc7482da625 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3762,13 +3762,13 @@ types-tqdm = "*" [[package]] name = "litellm" -version = "1.47.1" +version = "1.48.0" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" files = [ - {file = "litellm-1.47.1-py3-none-any.whl", hash = "sha256:baa1961287ee398c937e8a5ecd1fcb821ea8f91cbd1f4757b6c19d7bcc84d4fd"}, - {file = "litellm-1.47.1.tar.gz", hash = "sha256:51d1eb353573ddeac75c45b66147f533f64f231540667ea30b63edb9a2af15ce"}, + {file = "litellm-1.48.0-py3-none-any.whl", hash = "sha256:7765e8a92069778f5fc66aacfabd0e2f8ec8d74fb117f5e475567d89b0d376b9"}, + {file = "litellm-1.48.0.tar.gz", hash = "sha256:31a9b8a25a9daf44c24ddc08bf74298da920f2c5cea44135e5061278d0aa6fc9"}, ] [package.dependencies] From 63c5d741699b305b9cb55751608cc8d3ecc92163 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:06:20 +0200 Subject: [PATCH 05/12] chore(deps-dev): bump openai from 1.47.0 to 1.47.1 (#4033) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index dc7482da625..06698fdd594 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5365,13 +5365,13 @@ sympy = "*" [[package]] name = "openai" -version = "1.47.0" +version = "1.47.1" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.47.0-py3-none-any.whl", hash = "sha256:9ccc8737dfa791f7bd903db4758c176b8544a8cd89d3a3d2add3cea02a34c3a0"}, - {file = "openai-1.47.0.tar.gz", hash = "sha256:6e14d6f77c8cf546646afcd87a2ef752505b3710d2564a2e433e17307dfa86a0"}, + {file = "openai-1.47.1-py3-none-any.whl", hash = "sha256:34277583bf268bb2494bc03f48ac123788c5e2a914db1d5a23d5edc29d35c825"}, + {file = "openai-1.47.1.tar.gz", hash = "sha256:62c8f5f478f82ffafc93b33040f8bb16a45948306198bd0cba2da2ecd9cf7323"}, ] [package.dependencies] From f2a71eb388438b76d6854f57747a98c5f0196e1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:06:52 +0200 Subject: [PATCH 06/12] chore(deps): bump boto3 from 1.35.24 to 1.35.25 (#4027) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 06698fdd594..4610ae1b6b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -571,17 +571,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.24" +version = "1.35.25" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.24-py3-none-any.whl", hash = "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b"}, - {file = "boto3-1.35.24.tar.gz", hash = "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40"}, + {file = "boto3-1.35.25-py3-none-any.whl", hash = "sha256:b1cfad301184cdd44dfd4805187ccab12de8dd28dd12a11a5cfdace17918c6de"}, + {file = "boto3-1.35.25.tar.gz", hash = "sha256:5df4e2cbe3409db07d3a0d8d63d5220ce3202a78206ad87afdbb41519b26ce45"}, ] [package.dependencies] -botocore = ">=1.35.24,<1.36.0" +botocore = ">=1.35.25,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -590,13 +590,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.24" +version = "1.35.25" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.24-py3-none-any.whl", hash = "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3"}, - {file = "botocore-1.35.24.tar.gz", hash = "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b"}, + {file = "botocore-1.35.25-py3-none-any.whl", hash = "sha256:e58d60260abf10ccc4417967923117c9902a6a0cff9fddb6ea7ff42dc1bd4630"}, + {file = "botocore-1.35.25.tar.gz", hash = "sha256:76c5706b2c6533000603ae8683a297c887abbbaf6ee31e1b2e2863b74b2989bc"}, ] [package.dependencies] From 2f1b537471a310d76a0131e769e7ddbdf243d05b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:07:24 +0200 Subject: [PATCH 07/12] chore(deps): bump minio from 7.2.8 to 7.2.9 (#4034) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4610ae1b6b4..b2ddeb247f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4539,13 +4539,13 @@ files = [ [[package]] name = "minio" -version = "7.2.8" +version = "7.2.9" description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" optional = false python-versions = ">3.8" files = [ - {file = "minio-7.2.8-py3-none-any.whl", hash = "sha256:aa3b485788b63b12406a5798465d12a57e4be2ac2a58a8380959b6b748e64ddd"}, - {file = "minio-7.2.8.tar.gz", hash = "sha256:f8af2dafc22ebe1aef3ac181b8e217037011c430aa6da276ed627e55aaf7c815"}, + {file = "minio-7.2.9-py3-none-any.whl", hash = "sha256:fe5523d9c4a4d6cfc07e96905852841bccdb22b22770e1efca4bf5ae8b65774b"}, + {file = "minio-7.2.9.tar.gz", hash = "sha256:a83c2fcd981944602a8dc11e8e07543ed9cda0a9462264e3f46a13171c56bccb"}, ] [package.dependencies] From 7b2b1eff57e41364b4b427e36e766607e7eed3a0 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 24 Sep 2024 14:18:19 -0400 Subject: [PATCH 08/12] fix up settings saves (#4037) --- frontend/src/components/modals/settings/SettingsForm.tsx | 2 -- frontend/src/services/settings.ts | 8 ++++---- openhands/server/session/session.py | 4 +--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/modals/settings/SettingsForm.tsx b/frontend/src/components/modals/settings/SettingsForm.tsx index aa1fa95d552..12f1fb697b8 100644 --- a/frontend/src/components/modals/settings/SettingsForm.tsx +++ b/frontend/src/components/modals/settings/SettingsForm.tsx @@ -54,8 +54,6 @@ function SettingsForm({ const handleAdvancedChange = (value: boolean) => { setEnableAdvanced(value); - // Reset the base URL if the user disables advanced options - if (!value) onBaseURLChange(""); }; return ( diff --git a/frontend/src/services/settings.ts b/frontend/src/services/settings.ts index 4d06f202858..0e769faaba3 100644 --- a/frontend/src/services/settings.ts +++ b/frontend/src/services/settings.ts @@ -87,10 +87,10 @@ export const getSettings = (): Settings => { export const saveSettings = (settings: Partial) => { Object.keys(settings).forEach((key) => { const isValid = validKeys.includes(key as keyof Settings); - const value = settings[key as keyof Settings]; - - if (isValid && typeof value !== "undefined") - localStorage.setItem(key, value.toString()); + if (!isValid) return; + let value = settings[key as keyof Settings]; + if (value === undefined || value === null) value = ""; + localStorage.setItem(key, value.toString()); }); localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString()); }; diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index 10c51594db5..588df196108 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -76,9 +76,7 @@ async def _initialize_agent(self, data: dict): AgentStateChangedObservation('', AgentState.LOADING), EventSource.AGENT ) # Extract the agent-relevant arguments from the request - args = { - key: value for key, value in data.get('args', {}).items() if value != '' - } + args = {key: value for key, value in data.get('args', {}).items()} agent_cls = args.get(ConfigType.AGENT, self.config.default_agent) self.config.security.confirmation_mode = args.get( ConfigType.CONFIRMATION_MODE, self.config.security.confirmation_mode 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 09/12] (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 fe7501ff2fd..5cc736a6439 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 5ebf5546e82..d56b0b4825b 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 9c38e3e20bf..7ae2b021c9c 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 8d1f2617fa1..795c60e051f 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 72b1a4a8cf5..1f2e99a3796 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 392905eaa39..8e77a33cf2f 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 00000000000..5517d6af863 --- /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 7fffbfb5701..0de8d08d075 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 515441c74c1..a7a062cd6ec 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 b635d78c337..cad6131f80e 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 c25ba9a0d81..3aa6b5ef180 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 987fddf9095..1b34eb5b538 100644 --- a/openhands/runtime/client/client.py +++ b/openhands/runtime/client/client.py @@ -16,8 +16,10 @@ 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 @@ async def lifespan(app: FastAPI): 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 6a8d5eea3e6..bb2c78b79af 100644 --- a/openhands/runtime/client/runtime.py +++ b/openhands/runtime/client/runtime.py @@ -2,6 +2,7 @@ import tempfile import threading import uuid +from typing import Callable from zipfile import ZipFile import docker @@ -119,6 +120,7 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( ) # 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 @@ def __init__( 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 @@ def _init_container( 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 @@ def _init_container( 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 @@ def _init_container( ) else: browsergym_arg = '' + container = self.docker_client.containers.run( self.runtime_container_image, command=( @@ -281,6 +292,7 @@ def _init_container( ) 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 @@ def _find_available_port(self, max_attempts=5): 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 82ca16f9391..d2988895ba5 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 @@ def __init__( 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 d37433c3d4d..9cc0ebe7bac 100644 --- a/openhands/runtime/remote/runtime.py +++ b/openhands/runtime/remote/runtime.py @@ -2,6 +2,7 @@ import tempfile import threading import uuid +from typing import Callable, Optional from zipfile import ZipFile import requests @@ -55,6 +56,7 @@ def __init__( 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 @@ def __init__( ) # 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 902e6027f20..9c7fbe54475 100644 --- a/openhands/runtime/runtime.py +++ b/openhands/runtime/runtime.py @@ -3,6 +3,7 @@ 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 @@ def __init__( 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 31a8a821cb0..bb55d37b2f0 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 @@ async def start( 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 @@ async def start( - 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 @@ async def _create_security_analyzer(self, security_analyzer: str | None): - 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 @@ async def _create_runtime(self, runtime_name: str, config: AppConfig, agent: Age """ 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 @@ async def _create_controller( ) 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 a14fdc8be17..99da3bc4cb6 100644 --- a/openhands/server/session/manager.py +++ b/openhands/server/session/manager.py @@ -35,9 +35,11 @@ def get_session(self, sid: str) -> Session | None: 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 588df196108..fd9e9aa578a 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -21,7 +21,7 @@ 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 @@ def __init__( 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 @@ async def _initialize_agent(self, data: dict): 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 @@ async def _initialize_agent(self, data: dict): ) 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 @@ async def on_event(self, event: Event): 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 @@ async def send(self, data: dict[str, object]) -> bool: 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 @@ def load_from_data(self, data: dict) -> bool: 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) From 1b1d8f0b02e5e6103f2cd3cbf250b8ec4d034ebc Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Tue, 24 Sep 2024 15:47:27 -0500 Subject: [PATCH 10/12] [eval] Use `imap_unorderd` for parallizing evaluation (#4040) --- evaluation/utils/shared.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py index d3850882882..2981e557990 100644 --- a/evaluation/utils/shared.py +++ b/evaluation/utils/shared.py @@ -301,6 +301,11 @@ def _process_instance_wrapper( time.sleep(5) +def _process_instance_wrapper_mp(args): + """Wrapper for multiprocessing, especially for imap_unordered.""" + return _process_instance_wrapper(*args) + + def run_evaluation( dataset: pd.DataFrame, metadata: EvalMetadata | None, @@ -328,21 +333,13 @@ def run_evaluation( try: if use_multiprocessing: with mp.Pool(num_workers) as pool: - results = [ - pool.apply_async( - _process_instance_wrapper, - args=( - process_instance_func, - instance, - metadata, - True, - max_retries, - ), - ) + args_iter = ( + (process_instance_func, instance, metadata, True, max_retries) for _, instance in dataset.iterrows() - ] + ) + results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter) for result in results: - update_progress(result.get(), pbar, output_fp) + update_progress(result, pbar, output_fp) else: for _, instance in dataset.iterrows(): result = _process_instance_wrapper( From ee284bae8f6f30235e89022c63b88f05092a9239 Mon Sep 17 00:00:00 2001 From: tofarr Date: Tue, 24 Sep 2024 15:49:30 -0600 Subject: [PATCH 11/12] Fix server lock up on session init (#4007) --- openhands/controller/agent_controller.py | 7 ++--- openhands/core/cli.py | 3 +++ openhands/core/main.py | 3 +++ openhands/server/session/agent_session.py | 33 +++++++++++++++++------ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index 724d2c36f37..29bb27e2201 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -54,7 +54,7 @@ class AgentController: confirmation_mode: bool agent_to_llm_config: dict[str, LLMConfig] agent_configs: dict[str, AgentConfig] - agent_task: asyncio.Task | None = None + agent_task: asyncio.Future | None = None parent: 'AgentController | None' = None delegate: 'AgentController | None' = None _pending_action: Action | None = None @@ -115,9 +115,6 @@ def __init__( # stuck helper self._stuck_detector = StuckDetector(self.state) - if not is_delegate: - self.agent_task = asyncio.create_task(self._start_step_loop()) - async def close(self): """Closes the agent controller, canceling any ongoing tasks and unsubscribing from the event stream.""" if self.agent_task is not None: @@ -149,7 +146,7 @@ async def report_error(self, message: str, exception: Exception | None = None): self.state.last_error += f': {exception}' self.event_stream.add_event(ErrorObservation(message), EventSource.AGENT) - async def _start_step_loop(self): + async def start_step_loop(self): """The main loop for the agent's step-by-step execution.""" logger.info(f'[Agent Controller {self.id}] Starting step loop...') diff --git a/openhands/core/cli.py b/openhands/core/cli.py index 071ae248eea..7c4380a27aa 100644 --- a/openhands/core/cli.py +++ b/openhands/core/cli.py @@ -121,6 +121,9 @@ async def main(): event_stream=event_stream, ) + if controller is not None: + controller.agent_task = asyncio.create_task(controller.start_step_loop()) + async def prompt_for_next_task(): next_message = input('How can I help? >> ') if next_message == 'exit': diff --git a/openhands/core/main.py b/openhands/core/main.py index 3aa6b5ef180..2b03f2f7a90 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -143,6 +143,9 @@ async def run_controller( headless_mode=headless_mode, ) + if controller is not None: + controller.agent_task = asyncio.create_task(controller.start_step_loop()) + assert isinstance(task_str, str), f'task_str must be a string, got {type(task_str)}' # Logging logger.info( diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index bb55d37b2f0..976b285f54a 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -1,4 +1,6 @@ import asyncio + +from threading import Thread from typing import Callable, Optional from openhands.controller import AgentController @@ -65,9 +67,14 @@ async def start( 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, status_message_callback) - await self._create_controller( + + self.loop = asyncio.new_event_loop() + self.thread = Thread(target=self._run, daemon=True) + self.thread.start() + + self._create_security_analyzer(config.security.security_analyzer) + self._create_runtime(runtime_name, config, agent, status_message_callback) + self._create_controller( agent, config.security.confirmation_mode, max_iterations, @@ -75,6 +82,13 @@ async def start( agent_to_llm_config=agent_to_llm_config, agent_configs=agent_configs, ) + + if self.controller is not None: + self.controller.agent_task = asyncio.run_coroutine_threadsafe(self.controller.start_step_loop(), self.loop) # type: ignore + + def _run(self): + asyncio.set_event_loop(self.loop) + self.loop.run_forever() async def close(self): """Closes the Agent session""" @@ -89,9 +103,13 @@ async def close(self): self.runtime.close() if self.security_analyzer is not None: await self.security_analyzer.close() + + self.loop.call_soon_threadsafe(self.loop.stop) + self.thread.join() + self._closed = True - async def _create_security_analyzer(self, security_analyzer: str | None): + def _create_security_analyzer(self, security_analyzer: str | None): """Creates a SecurityAnalyzer instance that will be used to analyze the agent actions Parameters: @@ -104,7 +122,7 @@ async def _create_security_analyzer(self, security_analyzer: str | None): security_analyzer, SecurityAnalyzer )(self.event_stream) - async def _create_runtime( + def _create_runtime( self, runtime_name: str, config: AppConfig, @@ -125,8 +143,7 @@ async def _create_runtime( logger.info(f'Initializing runtime `{runtime_name}` now...') runtime_cls = get_runtime_cls(runtime_name) - self.runtime = await asyncio.to_thread( - runtime_cls, + self.runtime = runtime_cls( config=config, event_stream=self.event_stream, sid=self.sid, @@ -141,7 +158,7 @@ async def _create_runtime( else: logger.warning('Runtime initialization failed') - async def _create_controller( + def _create_controller( self, agent: Agent, confirmation_mode: bool, From 1d052818ae51856c13e6d468ab79673747440ae5 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Tue, 24 Sep 2024 23:20:45 -0400 Subject: [PATCH 12/12] Set runtime container image so it doesn't need to be rebuilt (#4035) --- .github/workflows/ghcr_runtime.yml | 16 +++++++--------- tests/runtime/conftest.py | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ghcr_runtime.yml b/.github/workflows/ghcr_runtime.yml index cf49c2384ab..8d6f622058b 100644 --- a/.github/workflows/ghcr_runtime.yml +++ b/.github/workflows/ghcr_runtime.yml @@ -145,8 +145,7 @@ jobs: run: make install-python-dependencies - name: Run runtime tests run: | - # We install pytest-xdist in order to run tests across CPUs. However, tests start to fail when we run - # then across more than 2 CPUs for some reason + # We install pytest-xdist in order to run tests across CPUs poetry run pip install pytest-xdist # Install to be able to retry on failures for flaky tests @@ -158,10 +157,10 @@ jobs: SKIP_CONTAINER_LOGS=true \ TEST_RUNTIME=eventstream \ SANDBOX_USER_ID=$(id -u) \ - SANDBOX_BASE_CONTAINER_IMAGE=$image_name \ + SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \ TEST_IN_CI=true \ RUN_AS_OPENHANDS=false \ - poetry run pytest -n 3 --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime + poetry run pytest -n 3 -raR --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 env: @@ -207,8 +206,7 @@ jobs: run: make install-python-dependencies - name: Run runtime tests run: | - # We install pytest-xdist in order to run tests across CPUs. However, tests start to fail when we run - # then across more than 2 CPUs for some reason + # We install pytest-xdist in order to run tests across CPUs poetry run pip install pytest-xdist # Install to be able to retry on failures for flaky tests @@ -220,10 +218,10 @@ jobs: SKIP_CONTAINER_LOGS=true \ TEST_RUNTIME=eventstream \ SANDBOX_USER_ID=$(id -u) \ - SANDBOX_BASE_CONTAINER_IMAGE=$image_name \ + SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \ TEST_IN_CI=true \ RUN_AS_OPENHANDS=true \ - poetry run pytest -n 3 --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime + poetry run pytest -n 3 -raR --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 env: @@ -275,7 +273,7 @@ jobs: TEST_RUNTIME=eventstream \ SANDBOX_USER_ID=$(id -u) \ - SANDBOX_BASE_CONTAINER_IMAGE=$image_name \ + SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \ TEST_IN_CI=true \ TEST_ONLY=true \ ./tests/integration/regenerate.sh diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py index 6ce93256e4a..2308244fb35 100644 --- a/tests/runtime/conftest.py +++ b/tests/runtime/conftest.py @@ -243,6 +243,7 @@ def _load_runtime( if base_container_image is not None: config.sandbox.base_container_image = base_container_image + config.sandbox.runtime_container_image = None file_store = get_file_store(config.file_store, config.file_store_path) event_stream = EventStream(sid, file_store)