diff --git a/config.template.toml b/config.template.toml
index 1b6cd11c9a5b..0c83cc6ccdd5 100644
--- a/config.template.toml
+++ b/config.template.toml
@@ -200,6 +200,7 @@ model = "gpt-4o"
# https://github.com/All-Hands-AI/OpenHands/pull/4711
#native_tool_calling = None
+
# Safety settings for models that support them (e.g., Mistral AI, Gemini)
# Example for Mistral AI:
# safety_settings = [
@@ -218,6 +219,9 @@ model = "gpt-4o"
# ]
#safety_settings = []
+[llm.draft_editor]
+# The number of times llm_editor tries to fix an error when editing.
+correct_num = 5
[llm.gpt4o-mini]
api_key = ""
@@ -335,6 +339,9 @@ classpath = "my_package.my_module.MyCustomAgent"
# Enable GPU support in the runtime
#enable_gpu = false
+# When there are multiple cards, you can specify the GPU by ID
+#cuda_visible_devices = ''
+
# Additional Docker runtime kwargs
#docker_runtime_kwargs = {}
diff --git a/openhands/core/config/sandbox_config.py b/openhands/core/config/sandbox_config.py
index a57422325d5c..41b5016864c9 100644
--- a/openhands/core/config/sandbox_config.py
+++ b/openhands/core/config/sandbox_config.py
@@ -88,6 +88,7 @@ class SandboxConfig(BaseModel):
description="Volume mounts in the format 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'. Multiple mounts can be specified using commas, e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'",
)
+ cuda_visible_devices: str | None = Field(default=None)
model_config = ConfigDict(extra='forbid')
@classmethod
diff --git a/openhands/runtime/impl/docker/docker_runtime.py b/openhands/runtime/impl/docker/docker_runtime.py
index bac1ef1f2326..2d74339d5587 100644
--- a/openhands/runtime/impl/docker/docker_runtime.py
+++ b/openhands/runtime/impl/docker/docker_runtime.py
@@ -360,7 +360,21 @@ def init_container(self) -> None:
)
command = self.get_action_execution_server_startup_command()
-
+ if self.config.sandbox.enable_gpu:
+ gpu_ids = self.config.sandbox.cuda_visible_devices
+ if gpu_ids is None:
+ device_requests = [
+ docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)
+ ]
+ else:
+ device_requests = [
+ docker.types.DeviceRequest(
+ capabilities=[['gpu']],
+ device_ids=[str(i) for i in gpu_ids.split(',')],
+ )
+ ]
+ else:
+ device_requests = None
try:
if self.runtime_container_image is None:
raise ValueError('Runtime container image is not set')
@@ -376,11 +390,7 @@ def init_container(self) -> None:
detach=True,
environment=environment,
volumes=volumes, # type: ignore
- device_requests=(
- [docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)]
- if self.config.sandbox.enable_gpu
- else None
- ),
+ device_requests=device_requests,
**(self.config.sandbox.docker_runtime_kwargs or {}),
)
self.log('debug', f'Container started. Server url: {self.api_url}')
diff --git a/openhands/runtime/utils/edit.py b/openhands/runtime/utils/edit.py
index d5d71407bc50..367fb623a2ed 100644
--- a/openhands/runtime/utils/edit.py
+++ b/openhands/runtime/utils/edit.py
@@ -39,6 +39,40 @@
{draft_changes}
"""
+CORRECT_SYS_MSG = """You are a code repair assistant. Now you have an original file content and error information from a static code checking tool (lint tool). Your task is to automatically modify and return the repaired complete code based on these error messages and refer to the current file content.
+
+The following are the specific task steps you need to complete:
+
+Carefully read the current file content to ensure that you fully understand its code structure.
+
+According to the lint error prompt, accurately locate and analyze the cause of the problem.
+
+Modify the original file content and fix all errors prompted by the lint tool.
+
+Return complete, runnable, and error-fixed code, paying attention to maintaining the overall style and specifications of the original code.
+
+Please note:
+
+Please strictly follow the lint error prompts to make modifications and do not miss any problems.
+
+The modified code must be complete and cannot introduce new errors or bugs.
+
+The modified code must maintain the original code function and logic, and no changes unrelated to error repair should be made."""
+
+CORRECT_USER_MSG = """
+THE FOLLOWING ARE THE ORIGINAL FILE CONTENTS AND THE ERROR INFORMATION REPORTED BY THE LINT TOOL
+
+# CURRENT FILE CONTENT:
+```
+{file_content}
+```
+
+# ERROR MESSAGE FROM STATIC CODE CHECKING TOOL:
+```
+{lint_error}
+```
+""".strip()
+
def _extract_code(string: str) -> str | None:
pattern = r'(.*?)'
@@ -196,7 +230,7 @@ def _get_lint_error(
return ErrorObservation(error_message)
return None
- def llm_based_edit(self, action: FileEditAction) -> Observation:
+ def llm_based_edit(self, action: FileEditAction, retry_num: int = 0) -> Observation:
obs = self.read(FileReadAction(path=action.path))
if (
isinstance(obs, ErrorObservation)
@@ -253,7 +287,14 @@ def llm_based_edit(self, action: FileEditAction) -> Observation:
diff,
)
if error_obs is not None:
- return error_obs
+ self.write(
+ FileWriteAction(path=action.path, content=updated_content)
+ )
+ return self.correct_edit(
+ file_content=updated_content,
+ error_obs=error_obs,
+ retry_num=retry_num,
+ )
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
return FileEditObservation(
@@ -280,7 +321,8 @@ def llm_based_edit(self, action: FileEditAction) -> Observation:
error_msg = (
f'[Edit error: The range of lines to edit is too long.]\n'
f'[The maximum number of lines allowed to edit at once is {self.MAX_LINES_TO_EDIT}. '
- f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n' # [start_idx, end_idx), so no need to + 1
+ f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n'
+ # [start_idx, end_idx), so no need to + 1
)
# search for relevant ranges to hint the agent
topk_chunks: list[Chunk] = get_top_k_chunk_matches(
@@ -333,7 +375,12 @@ def llm_based_edit(self, action: FileEditAction) -> Observation:
)
if error_obs is not None:
error_obs.llm_metrics = self.draft_editor_llm.metrics
- return error_obs
+ self.write(FileWriteAction(path=action.path, content=updated_content))
+ return self.correct_edit(
+ file_content=updated_content,
+ error_obs=error_obs,
+ retry_num=retry_num,
+ )
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
ret_obs = FileEditObservation(
@@ -345,3 +392,40 @@ def llm_based_edit(self, action: FileEditAction) -> Observation:
)
ret_obs.llm_metrics = self.draft_editor_llm.metrics
return ret_obs
+
+ def check_retry_num(self, retry_num):
+ correct_num = self.draft_editor_llm.config.correct_num
+ return correct_num < retry_num
+
+ def correct_edit(
+ self, file_content: str, error_obs: ErrorObservation, retry_num: int = 0
+ ) -> Observation:
+ import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
+ from openhands.agenthub.codeact_agent.tools import LLMBasedFileEditTool
+ from openhands.llm.llm_utils import check_tools
+
+ _retry_num = retry_num + 1
+ if self.check_retry_num(_retry_num):
+ return error_obs
+ tools = check_tools([LLMBasedFileEditTool], self.draft_editor_llm.config)
+ messages = [
+ {'role': 'system', 'content': CORRECT_SYS_MSG},
+ {
+ 'role': 'user',
+ 'content': CORRECT_USER_MSG.format(
+ file_content=file_content, lint_error=error_obs.content
+ ),
+ },
+ ]
+ params: dict = {'messages': messages, 'tools': tools}
+ try:
+ response = self.draft_editor_llm.completion(**params)
+ actions = codeact_function_calling.response_to_actions(response)
+ if len(actions) != 1:
+ return error_obs
+ for action in actions:
+ if isinstance(action, FileEditAction):
+ return self.llm_based_edit(action, _retry_num)
+ except Exception as e:
+ logger.error(f'correct lint error is failed: {e}')
+ return error_obs