diff --git a/examples/deepswe/openhands_utils.py b/examples/deepswe/openhands_utils.py index cee4395cba..2f76a898b8 100644 --- a/examples/deepswe/openhands_utils.py +++ b/examples/deepswe/openhands_utils.py @@ -15,13 +15,69 @@ """Utility functions for OpenHands workspace and environment setup.""" import base64 +import json import logging import os +import re from typing import Any, Optional from tunix.rl.agentic.environments.base_environment import EnvStepResult +def _get_swe_action_cls() -> Any: + try: + from r2egym.agenthub.action import Action as SWEAction # pytype: disable=import-error + return SWEAction + except ImportError: + from examples.deepswe import swe_agent + return swe_agent.SWEAction + + +def parse_openhands_action_str(action_str: str) -> Any: + """Parses an XML action string into an Action object without stripping indentation.""" + swe_action_cls = _get_swe_action_cls() + + fn_match = re.search(r"]+)>", action_str) + function_name = fn_match.group(1).strip() if fn_match else "" + + pattern = r"]+)>(.*?)" + param_matches = re.findall(pattern, action_str, flags=re.DOTALL) + + params: dict[str, str] = {} + for param_key, param_value in param_matches: + param_key = param_key.strip() + if param_value.startswith("\r\n"): + param_value = param_value[2:] + elif param_value.startswith("\n"): + param_value = param_value[1:] + if param_value.endswith("\r\n"): + param_value = param_value[:-2] + elif param_value.endswith("\n"): + param_value = param_value[:-1] + params[param_key] = param_value + + return swe_action_cls(function_name, params) + + +def resolve_base_commit(entry: Optional[dict[str, Any]]) -> str: + """Resolves base_commit from dataset entry metadata if available.""" + if not isinstance(entry, dict): + return "" + base_commit = entry.get("base_commit") + if base_commit: + return str(base_commit).strip() + parsed_commit = entry.get("parsed_commit_content") + if parsed_commit: + if isinstance(parsed_commit, str): + try: + parsed_commit = json.loads(parsed_commit) + except Exception: # pylint: disable=broad-exception-caught + parsed_commit = None + if isinstance(parsed_commit, dict) and parsed_commit.get("old_commit_hash"): + return str(parsed_commit["old_commit_hash"]).strip() + return "" + + def get_image_rewrite_fn(image_rewrite: Any | None = None) -> Any | None: """Retrieve or construct the image rewrite function from prefix if configured.""" if image_rewrite is not None: @@ -162,9 +218,9 @@ def step_openhands( ) -> EnvStepResult: """Execute an action in an OpenHands-backed environment. - Handles OpenHands-specific tool dispatch ('finish'/'submit', - 'str_replace_editor'/'file_editor', 'execute_ipython_cell', and - 'execute_bash') using the environment's workspace and bound grading + Handles OpenHands-specific tool dispatch ('finish'/'submit', 'think', + 'task_tracker', 'str_replace_editor'/'file_editor', 'execute_ipython_cell', + and 'execute_bash') using the environment's workspace and bound grading environment. Args: @@ -176,20 +232,91 @@ def step_openhands( EnvStepResult: Result of executing the action. """ max_steps = getattr(env, "max_steps", None) + params = getattr(action_obj, "parameters", None) or {} if action_obj.function_name in ("finish", "submit"): + msg = params.get("message") or params.get("result") or "Task submitted." return EnvStepResult( - observation="Task submitted.", + observation=str(msg), reward=0, done=True, info={"max_steps": max_steps}, ) + if action_obj.function_name == "think": + if hasattr(env, "total_steps"): + env.total_steps += 1 + return EnvStepResult( + observation="Your thought has been logged.", + reward=0, + done=False, + info={"max_steps": max_steps}, + ) + + if action_obj.function_name == "task_tracker": + cmd = params.get("command", "view") + if hasattr(env, "total_steps"): + env.total_steps += 1 + if cmd == "view": + content = getattr(env, "_task_list_content", None) + obs = ( + content + if isinstance(content, str) and content + else 'No task list found. Use the "plan" command to create one.' + ) + return EnvStepResult( + observation=obs, reward=0, done=False, info={"max_steps": max_steps} + ) + if cmd == "plan": + raw_list = params.get("task_list", "[]") + if isinstance(raw_list, str): + try: + task_list = json.loads(raw_list) + except Exception as e: # pylint: disable=broad-exception-caught + return EnvStepResult( + observation=f"Error: Failed to parse task_list JSON: {e}", + reward=0, + done=False, + info={"max_steps": max_steps}, + ) + elif isinstance(raw_list, list): + task_list = raw_list + else: + task_list = [] + status_icons = {"todo": "⏳", "in_progress": "🔄", "done": "✅"} + content = "# Task List\n\n" + for i, item in enumerate(task_list, 1): + if isinstance(item, dict): + icon = status_icons.get(str(item.get("status", "todo")), "⏳") + content += f"{i}. {icon} {item.get('title', '')}\n{item.get('notes', '')}\n" + env._task_list = task_list + env._task_list_content = content + return EnvStepResult( + observation=( + f"Task list has been updated with {len(task_list)} items." + " Stored in session directory: /workspace/.openhands/TASKS.md" + ), + reward=0, + done=False, + info={"max_steps": max_steps}, + ) + return EnvStepResult( + observation=( + f"Error: Invalid task_tracker command '{cmd}'. Must be 'view' or" + " 'plan'." + ), + reward=0, + done=False, + info={"max_steps": max_steps}, + ) + if action_obj.function_name in ("str_replace_editor", "file_editor") and env.env is not None: # R2E registers this editor as `file_editor` and `RepoEnv.run_action` # asserts the tool name is in its registered command list, so translate - # before delegating. The parameter schemas are identical. + # before delegating. action_obj.function_name = "file_editor" + if isinstance(getattr(action_obj, "parameters", None), dict): + action_obj.parameters.pop("security_risk", None) try: obs, _, done, _ = env.env.step(action_obj) obs_str = str(obs) @@ -221,7 +348,7 @@ def step_openhands( info={"max_steps": max_steps}, ) - step_timeout = getattr(env, "step_timeout", 30.0) + step_timeout = getattr(env, "step_timeout", 60.0) b64_code = base64.b64encode(code.encode("utf-8")).decode("ascii") wrapped_cmd = ( "(cd /testbed 2>/dev/null || cd /workspace) && " @@ -254,8 +381,8 @@ def step_openhands( info={"max_steps": max_steps}, ) elif getattr(env, "env", None) is not None: - from r2egym.agenthub.action.action import Action as SWEAction # pytype: disable=import-error - bash_action = SWEAction("execute_bash", {"command": wrapped_cmd}) + swe_action_cls = _get_swe_action_cls() + bash_action = swe_action_cls("execute_bash", {"command": wrapped_cmd}) try: obs, reward, done, info = env.env.step(bash_action) obs_str = str(obs) @@ -284,7 +411,15 @@ def step_openhands( info={"max_steps": max_steps}, ) - step_timeout = getattr(env, "step_timeout", 30.0) + step_timeout = float(getattr(env, "step_timeout", 60.0)) + raw_timeout = params.get("timeout") + if raw_timeout is not None: + try: + req_timeout = float(raw_timeout) + if req_timeout > 0: + step_timeout = min(req_timeout, step_timeout) + except (ValueError, TypeError): + pass wrapped_cmd = f"(cd /testbed 2>/dev/null || cd /workspace) && {cmd}" if getattr(env, "workspace", None) is not None: @@ -313,6 +448,10 @@ def step_openhands( info={"max_steps": max_steps}, ) elif getattr(env, "env", None) is not None: + if isinstance(getattr(action_obj, "parameters", None), dict): + action_obj.parameters.pop("security_risk", None) + action_obj.parameters.pop("timeout", None) + action_obj.parameters.pop("is_input", None) try: obs, reward, done, info = env.env.step(action_obj) obs_str = str(obs) @@ -330,8 +469,9 @@ def step_openhands( return EnvStepResult( observation=( f"ERROR: Tool '{action_obj.function_name}' is not recognized. " - "Only 'execute_bash', 'execute_ipython_cell', 'str_replace_editor', " - "and 'submit' are available." + "Only 'execute_bash', 'think', 'finish', 'task_tracker', " + "'str_replace_editor', 'execute_ipython_cell', and 'submit' are" + " available." ), reward=0, done=False, diff --git a/examples/deepswe/swe_agent.py b/examples/deepswe/swe_agent.py index 0dbcbc8683..e2ff7bf64a 100644 --- a/examples/deepswe/swe_agent.py +++ b/examples/deepswe/swe_agent.py @@ -14,6 +14,9 @@ OPENHANDS_SCAFFOLDS = template.OPENHANDS_SCAFFOLDS OPENHANDS_SYSTEM_PROMPT = template.OPENHANDS_SYSTEM_PROMPT +OPENHANDS_USER_PROMPT = template.OPENHANDS_USER_PROMPT +OPENHANDS_FAKE_USER_RESPONSE = template.OPENHANDS_FAKE_USER_RESPONSE +OPENHANDS_TOOLS = template.OPENHANDS_TOOLS SWE_SYSTEM_PROMPT = template.SWE_SYSTEM_PROMPT SWE_SYSTEM_PROMPT_FN_CALL = template.SWE_SYSTEM_PROMPT_FN_CALL SWE_USER_PROMPT = template.SWE_USER_PROMPT @@ -22,6 +25,7 @@ SWEAGENT_USER_PROMPT = template.SWEAGENT_USER_PROMPT get_system_prompt = template.get_system_prompt get_user_prompt_template = template.get_user_prompt_template +format_openhands_user_prompt = template.format_openhands_user_prompt @@ -34,10 +38,47 @@ try: from r2egym.agenthub.action import Action as SWEAction # pytype: disable=import-error except ImportError: - logging.error( - "Failed to load SWEAction. Please ensure 'r2egym' is installed properly." - ) - raise # This halts execution and preserves the original traceback + + class SWEAction: # type: ignore[no-redef] + """Fallback Action representation when r2egym is not installed.""" + + def __init__( + self, + function_name: str = "", + parameters: Optional[dict[str, Any]] = None, + ): + self.function_name = function_name + self.parameters = parameters if parameters is not None else {} + + @classmethod + def from_string(cls, action_str: str) -> "SWEAction": + if not action_str or not action_str.strip(): + return cls("", {}) + fn_match = re.search(r"]+)>", action_str) + if not fn_match: + return cls("", {}) + fn_name = fn_match.group(1).strip() + params = {} + for k, v in re.findall( + r"]+)>(.*?)", + action_str, + flags=re.DOTALL, + ): + params[k.strip()] = v.strip() + return cls(fn_name, params) + + def to_xml_string(self) -> str: + if not self.function_name: + return "" + parts = [f""] + for k, v in self.parameters.items(): + parts.append(f"{v}") + parts.append("") + return "\n".join(parts) + + def __str__(self) -> str: + return self.to_xml_string() + TOKEN_WARNING_THRESHOLD = 28000 @@ -86,14 +127,45 @@ def parse_xml_response(response_text: str) -> tuple[str, Any]: return thought, action +def parse_openhands_xml_action(action_str: str) -> SWEAction: + """Parses an XML function call while preserving multiline code indentation. + + Matches Qwen3XMLToolParser behavior by stripping only a single leading and + single trailing newline from parameter values rather than calling .strip(), + which would strip leading indentation from str_replace_editor's old_str / + new_str parameters. + """ + fn_match = re.search(r"]+)>", action_str) + function_name = fn_match.group(1).strip() if fn_match else "" + + pattern = r"]+)>(.*?)" + param_matches = re.findall(pattern, action_str, flags=re.DOTALL) + + params: dict[str, str] = {} + for param_key, param_value in param_matches: + param_key = param_key.strip() + if param_value.startswith("\r\n"): + param_value = param_value[2:] + elif param_value.startswith("\n"): + param_value = param_value[1:] + if param_value.endswith("\r\n"): + param_value = param_value[:-2] + elif param_value.endswith("\n"): + param_value = param_value[:-1] + params[param_key] = param_value + + return SWEAction(function_name, params) + + def parse_codeact_response(response_text: str) -> tuple[str, Any]: """Parses a model response in CodeAct / OpenHands format. Supports: - 1. XML function blocks: - 2. Tool calls: ... or ```json ... ``` with tool call schema - 3. Markdown code blocks: ```(bash|sh|shell|python|py|ipython) ... ``` - 4. Task completion indicators: COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT + 1. Qwen3/Qwen3.5 XML tool calls: ... + 2. Bare XML function blocks: + 3. JSON tool calls: {...} or ```json ... ``` + 4. Markdown code blocks: ```(bash|sh|shell|python|py|ipython) ... ``` + 5. Task completion indicators: COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT Returns: (thought, action): Tuple of reasoning string and SWEAction instance. @@ -113,14 +185,18 @@ def parse_codeact_response(response_text: str) -> tuple[str, Any]: def _collect_candidates(text_slice: str, offset: int): found = [] - xml_match = xml_pattern.search(text_slice) - if xml_match: - found.append((0, offset + xml_match.start(), "xml", xml_match)) - tc_match = tc_pattern.search(text_slice) if tc_match: found.append((0, offset + tc_match.start(), "tool_call", tc_match)) + xml_match = xml_pattern.search(text_slice) + if xml_match: + # Only add bare xml candidate if it is not already inside tc_match. + if not tc_match or not ( + tc_match.start() <= xml_match.start() <= tc_match.end() + ): + found.append((0, offset + xml_match.start(), "xml", xml_match)) + json_match = json_cb_pattern.search(text_slice) if json_match: try: @@ -166,13 +242,20 @@ def _collect_candidates(text_slice: str, offset: int): xml_str = payload.group(1).strip() if not xml_str.endswith(""): xml_str += "\n" - action = SWEAction.from_string(xml_str) + action = parse_openhands_xml_action(xml_str) return thought, action elif match_type in ("tool_call", "json_block"): thought = response_text[:match_start].strip() if match_type == "tool_call": raw_payload = payload.group(1).strip() + xml_in_tc = xml_pattern.search(raw_payload) + if xml_in_tc: + xml_str = xml_in_tc.group(1).strip() + if not xml_str.endswith(""): + xml_str += "\n" + action = parse_openhands_xml_action(xml_str) + return thought, action try: data = json.loads(raw_payload) except Exception: @@ -196,7 +279,13 @@ def _collect_candidates(text_slice: str, offset: int): args = {"command": args} if not isinstance(args, dict): args = {"command": str(args)} - action = SWEAction(fn_name, {str(k): str(v) for k, v in args.items()}) + normalized_args = {} + for k, v in args.items(): + if isinstance(v, (dict, list)): + normalized_args[str(k)] = json.dumps(v, ensure_ascii=False) + else: + normalized_args[str(k)] = str(v) + action = SWEAction(fn_name, normalized_args) return thought, action elif match_type == "code_block": @@ -306,10 +395,15 @@ def update_from_env( observation += ( "\nYou are running out of tokens. Stop exploring now. Do not call" " file_editor, str_replace_editor, or execute_bash again. You must" - " immediately submit using the submit tool. Output exactly this XML" + " immediately submit using the finish tool. Output exactly this XML" " and nothing else:\n" - "\n" + "\n" + "\n" + "\n" + "Task completed.\n" + "\n" "\n" + "\n" ) else: observation += ( @@ -380,10 +474,12 @@ def update_from_model(self, response: str, **kwargs): class CodeActAgent(SWEAgent): - """CodeActAgent for OpenHands. + """CodeActAgent for OpenHands matching nv-OpenHands@0d766ad0. - Executes code (Bash and Python/IPython) directly as its primary action space, - supporting markdown code blocks, JSON tool calls, and XML function calls. + Uses the OpenHands 5-tool action space (`execute_bash`, `think`, `finish`, + `task_tracker`, `str_replace_editor`), Qwen3/Qwen3.5 XML native tool-calling + format, `swe_default.j2` user prompt, and `role="tool"` (``) + observations. """ name = "codeact_agent" @@ -402,6 +498,47 @@ def __init__( scaffold=scaffold, ) + def update_from_env( + self, + observation: Any, + reward: float, + done: bool, + info: Optional[dict[str, Any]] = None, + **kwargs, + ) -> None: + observation = str(observation) + if info is None: + info = {} + if len(self._trajectory.steps) == 0: + if self.user_prompt_template == OPENHANDS_USER_PROMPT: + observation = format_openhands_user_prompt( + problem_statement=observation, + workspace_path=str(info.get("workspace_path") or "/testbed"), + repo_language=str(info.get("repo_language") or "python"), + base_commit=str(info.get("base_commit") or ""), + ) + else: + observation = self.user_prompt_template.format( + problem_statement=observation + ) + elif not observation and not self._trajectory.steps[-1].action: + observation = OPENHANDS_FAKE_USER_RESPONSE + + ConversationAgentBase.update_from_env(self, observation, reward, done, info) + self.cur_step = Step(observation=observation) + + def _observation_to_messages( + self, observation: Any, reward: float, done: bool, info: dict[str, Any] + ) -> None: + if len(self._trajectory.steps) == 0: + self._messages.append({"role": "user", "content": str(observation)}) + return + last_step = self._trajectory.steps[-1] + if info.get("is_fake_user_response") or not last_step.action: + self._messages.append({"role": "user", "content": str(observation)}) + else: + self._messages.append({"role": "tool", "content": str(observation)}) + def _parse_model_response( self, response: str | Any ) -> tuple[str, SWEAction]: @@ -415,6 +552,7 @@ def _parse_model_response( "SWEAgent", "parse_codeact_response", "parse_oai_response", + "parse_openhands_xml_action", "parse_xml_response", ] diff --git a/examples/deepswe/swe_agent_test.py b/examples/deepswe/swe_agent_test.py index f3a7b21db8..16deefd805 100644 --- a/examples/deepswe/swe_agent_test.py +++ b/examples/deepswe/swe_agent_test.py @@ -17,12 +17,13 @@ import os from unittest import mock from absl.testing import absltest -from r2egym.agenthub.action.action import Action as SWEAction from examples.deepswe import openhands_utils from examples.deepswe import swe_agent from examples.deepswe import swe_env from examples.deepswe import template +SWEAction = swe_agent.SWEAction + class SweAgentTest(absltest.TestCase): @@ -297,16 +298,173 @@ def test_parse_codeact_markdown_json_tool_call(self): self.assertEqual(action.function_name, "execute_bash") self.assertEqual(action.parameters.get("command"), "pwd") - def test_codeact_agent_token_warning(self): + def test_codeact_agent_no_synthetic_token_or_step_warning(self): agent = swe_agent.CodeActAgent() agent.update_from_env( - observation="Init", + observation="Fix the bug in foo.py", reward=0.0, done=False, - info={"cur_tokens": 30000}, + info={ + "cur_tokens": 30000, + "max_steps": 50, + "base_commit": "abc1234", + "workspace_path": "/workspace", + "repo_language": "python", + }, + ) + self.assertNotIn("You are running out of tokens", agent.cur_step.observation) + self.assertNotIn("Steps Remaining", agent.cur_step.observation) + self.assertIn("\n/workspace\n", agent.cur_step.observation) + self.assertIn( + "compare your changes with the base commit abc1234.", + agent.cur_step.observation, + ) + self.assertEqual(agent.chat_completions[1]["role"], "user") + + def test_parse_codeact_qwen35_xml_tool_call_and_preserve_indentation(self): + response = ( + "\nLet's replace the indented block in foo.py.\n\n\n" + "\n" + "\n" + "\nstr_replace\n\n" + "\n/workspace/foo.py\n\n" + "\n if x:\n return 1\n\n" + "\n if x:\n return 2\n\n" + "\nLOW\n\n" + "\n" + "" + ) + thought, action = swe_agent.parse_codeact_response(response) + self.assertIn("", thought) + self.assertEqual(action.function_name, "str_replace_editor") + self.assertEqual(action.parameters["command"], "str_replace") + self.assertEqual(action.parameters["path"], "/workspace/foo.py") + self.assertEqual(action.parameters["old_str"], " if x:\n return 1") + self.assertEqual(action.parameters["new_str"], " if x:\n return 2") + self.assertEqual(action.parameters["security_risk"], "LOW") + + reparsed = openhands_utils.parse_openhands_action_str(action.to_xml_string()) + self.assertEqual(reparsed.function_name, "str_replace_editor") + self.assertEqual(reparsed.parameters["old_str"], " if x:\n return 1") + self.assertEqual(reparsed.parameters["new_str"], " if x:\n return 2") + + def test_codeact_agent_tool_role_and_fake_user_response(self): + agent = swe_agent.CodeActAgent() + agent.update_from_env( + observation="Fix issue", + reward=0.0, + done=False, + info={"base_commit": "deadbeef"}, + ) + self.assertEqual(agent.chat_completions[1]["role"], "user") + + # Turn 1: model emits a tool call -> next observation has role="tool" + agent.update_from_model( + "\nCheck status\n\n\n" + "\n\n" + "\ngit status\n\n" + "\nLOW\n\n" + "\n" + ) + agent.update_from_env( + observation="On branch main", + reward=0.0, + done=False, + info={}, + ) + self.assertEqual(agent.chat_completions[-1]["role"], "tool") + self.assertEqual(agent.chat_completions[-1]["content"], "On branch main") + + # Turn 2: model emits no tool call -> next observation uses fake user response with role="user" + agent.update_from_model("\nI am thinking without calling a tool.\n\nJust text.") + agent.update_from_env( + observation="", + reward=0.0, + done=False, + info={}, + ) + self.assertEqual(agent.chat_completions[-1]["role"], "user") + self.assertEqual( + agent.chat_completions[-1]["content"], + template.OPENHANDS_FAKE_USER_RESPONSE, + ) + + def test_step_openhands_think_task_tracker_and_finish(self): + mock_env = mock.MagicMock() + mock_env.max_steps = 10 + mock_env.total_steps = 0 + + think_res = openhands_utils.step_openhands( + mock_env, SWEAction("think", {"thought": "Analyzing root cause"}) + ) + self.assertFalse(think_res.done) + self.assertEqual(think_res.observation, "Your thought has been logged.") + + view_empty_res = openhands_utils.step_openhands( + mock_env, SWEAction("task_tracker", {"command": "view"}) + ) + self.assertFalse(view_empty_res.done) + self.assertIn("No task list found", view_empty_res.observation) + + plan_res = openhands_utils.step_openhands( + mock_env, + SWEAction( + "task_tracker", + { + "command": "plan", + "task_list": '[{"id": "1", "title": "Fix bug", "status": "in_progress"}]', + }, + ), + ) + self.assertFalse(plan_res.done) + self.assertIn("Task list has been updated with 1 items.", plan_res.observation) + + view_after_plan_res = openhands_utils.step_openhands( + mock_env, SWEAction("task_tracker", {"command": "view"}) + ) + self.assertIn("Fix bug", view_after_plan_res.observation) + + finish_res = openhands_utils.step_openhands( + mock_env, + SWEAction("finish", {"message": "Fixed the issue and verified tests."}), + ) + self.assertTrue(finish_res.done) + self.assertEqual( + finish_res.observation, "Fixed the issue and verified tests." ) - self.assertIn("You are running out of tokens", agent.cur_step.observation) - self.assertIn("", agent.cur_step.observation) + + def test_openhands_system_prompt_and_tools_schema(self): + tool_names = [t["function"]["name"] for t in template.OPENHANDS_TOOLS] + self.assertEqual( + tool_names, + [ + "execute_bash", + "think", + "finish", + "task_tracker", + "str_replace_editor", + ], + ) + tools_by_name = {t["function"]["name"]: t["function"] for t in template.OPENHANDS_TOOLS} + self.assertEqual( + tools_by_name["execute_bash"]["parameters"]["required"], + ["command"], + ) + self.assertIn( + "security_risk", + tools_by_name["execute_bash"]["parameters"]["properties"], + ) + self.assertEqual( + tools_by_name["str_replace_editor"]["parameters"]["required"], + ["command", "path"], + ) + self.assertIn( + "security_risk", + tools_by_name["str_replace_editor"]["parameters"]["properties"], + ) + self.assertIn("# Tools\n\nYou have access to the following functions:\n\n", template.OPENHANDS_SYSTEM_PROMPT) + self.assertIn("", template.OPENHANDS_SYSTEM_PROMPT) + self.assertIn("60 seconds", template.OPENHANDS_SYSTEM_PROMPT) def test_sweagent_token_warning(self): agent = swe_agent.SWEAgent(scaffold="sweagent") @@ -486,4 +644,3 @@ def test_deepswe_dist_agent_registration_and_openhands_dispatch(self): if __name__ == "__main__": absltest.main() - diff --git a/examples/deepswe/swe_env.py b/examples/deepswe/swe_env.py index 09eb72de25..e30a740a0a 100644 --- a/examples/deepswe/swe_env.py +++ b/examples/deepswe/swe_env.py @@ -291,16 +291,40 @@ def _initial_observation(self) -> Any: # Polls docker runtime to get task instruction. return self.env.get_task_instruction() # pytype: disable=attribute-error + def reset(self) -> tuple[Any, dict[str, Any]]: + obs, info = super().reset() + if self.scaffold in template_mod.OPENHANDS_SCAFFOLDS: + base_commit = openhands_utils.resolve_base_commit(self.entry) + if base_commit: + info["base_commit"] = base_commit + if self.entry.get("repo_language"): + info["repo_language"] = str(self.entry["repo_language"]) + info["workspace_path"] = os.getenv("OPENHANDS_WORKING_DIR", "/testbed") + return obs, info + def _step_impl(self, action: Any) -> EnvStepResult: global Action if Action is None: from r2egym.agenthub.action import Action # pytype: disable=import-error if isinstance(action, str): - action_obj = Action.from_string(action) + if self.scaffold in template_mod.OPENHANDS_SCAFFOLDS: + action_obj = openhands_utils.parse_openhands_action_str(action) + else: + action_obj = Action.from_string(action) else: action_obj = action if not action_obj.function_name: + if self.scaffold in template_mod.OPENHANDS_SCAFFOLDS: + return EnvStepResult( + observation=template_mod.OPENHANDS_FAKE_USER_RESPONSE, + reward=0, + done=False, + info={ + "max_steps": self.max_steps, + "is_fake_user_response": True, + }, + ) return EnvStepResult( observation="", reward=0, diff --git a/examples/deepswe/template.py b/examples/deepswe/template.py index 58c7ec8932..f1a39f9dff 100644 --- a/examples/deepswe/template.py +++ b/examples/deepswe/template.py @@ -215,80 +215,658 @@ def get_agent_name(scaffold: str | None = None) -> str: """ -OPENHANDS_SYSTEM_PROMPT = """You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks. -You are provided a github issue and repository bash environment and is tasked to solve certain tasks (e.g., file localization, testcase generation, code repair and editing etc) to resolve the issue. +_SECURITY_RISK_DESC = ( + "The LLM's assessment of the safety risk of this action. See the" + " SECURITY_RISK_ASSESSMENT section in the system prompt for risk level" + " definitions." +) +_RISK_LEVELS = ["LOW", "MEDIUM", "HIGH"] + +_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session. + + +### Command Execution +* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together. +* Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands. +* Soft timeout: Commands have a soft timeout of 10 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details) +* Shell options: Do NOT use `set -e`, `set -eu`, or `set -euo pipefail` in shell scripts or commands in this environment. The runtime may not support them and can cause unusable shell sessions. If you want to run multi-line bash commands, write the commands to a file and then run it, instead. + +### Long-running Commands +* For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`. +* For commands that may run for a long time (e.g. installation or testing commands), or commands that run for a fixed amount of time (e.g. sleep), you should set the "timeout" parameter of your function call to an appropriate value. +* If a bash command returns exit code `-1`, this means the process hit the soft timeout and is not yet finished. By setting `is_input` to `true`, you can: + - Send empty `command` to retrieve additional logs + - Send text (set `command` to the text) to STDIN of the running process + - Send control commands like `C-c` (Ctrl+C), `C-d` (Ctrl+D), or `C-z` (Ctrl+Z) to interrupt the process + - If you do C-c, you can re-start the process with a longer "timeout" parameter to let it run to completion + +### Best Practices +* Directory verification: Before creating new directories or files, first verify the parent directory exists and is the correct location. +* Directory management: Try to maintain working directory by using absolute paths and avoiding excessive use of `cd`. + +### Output Handling +* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned. +""" -We have access to the following functions: +_THINK_DESCRIPTION = """Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. -–– BEGIN FUNCTION #1: execute_bash –– -Description: -Execute a bash command in the terminal. -Parameters: - 1. command (string, required) -The bash command to execute. For example: `pytest tests/test_foo.py`. If not provided, will show help. -–– END FUNCTION #1 –– +Common use cases: +1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective. +2. After receiving test results, use this tool to brainstorm ways to fix failing tests. +3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs. +4. When designing a new feature, use this tool to think through architecture decisions and implementation details. +5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses. -–– BEGIN FUNCTION #2: execute_ipython_cell –– -Description: -Execute a Python code cell in the interactive environment. -Parameters: - 1. code (string, required) -The Python code to execute. -–– END FUNCTION #2 –– +The tool simply logs your thought process for better transparency and does not execute any code or make changes.""" -–– BEGIN FUNCTION #3: str_replace_editor –– -Description: -Custom editing tool for viewing, creating and editing files +_FINISH_DESCRIPTION = """Signals the completion of the current task or conversation. + +Use this tool when: +- You have successfully completed the user's requested task +- You cannot proceed further due to technical limitations or missing information + +The message should include: +- A clear summary of actions taken and their results +- Any next steps for the user +- Explanation if you're unable to complete the task +- Any follow-up questions if more information is needed +""" + +_DETAILED_TASK_TRACKER_DESCRIPTION = """This tool provides structured task management capabilities for development workflows. +It enables systematic tracking of work items, progress monitoring, and efficient +organization of complex development activities. + +The tool maintains visibility into project status and helps communicate +progress effectively to users. + +## Application Guidelines + +Utilize this tool in the following situations: + +1. Multi-phase development work - When projects involve multiple sequential or + parallel activities +2. Complex implementation tasks - Work requiring systematic planning and + coordination across multiple components +3. Explicit user request for task organization - When users specifically ask + for structured task management +4. Multiple concurrent requirements - When users present several work items + that need coordination +5. Project initiation - Capture and organize user requirements at project start +6. Work commencement - Update task status to in_progress before beginning + implementation. Maintain focus by limiting active work to one task +7. Task completion - Update status to done and identify any additional work + that emerged during implementation + +## Situations Where Tool Usage Is Unnecessary + +Avoid using this tool when: + +1. Single atomic tasks that require no decomposition +2. Trivial operations where tracking adds no organizational value +3. Simple activities completable in minimal steps +4. Pure information exchange or discussion + +Note: For single straightforward tasks, proceed with direct implementation +rather than creating tracking overhead. + +## Usage Scenarios + +**Scenario A: Feature Development with Validation** +User request: "Build a user authentication system with login/logout functionality. +Don't forget to include input validation and error handling!" + +Response approach: I'll implement a user authentication system with comprehensive +validation. Let me organize this work systematically. + +*Task breakdown includes:* +1. Design authentication API endpoints and data models +2. Implement user login/logout controllers with session management +3. Add client-side form validation and error display +4. Create backend input sanitization and security checks +5. Implement comprehensive error handling for edge cases +6. Perform integration testing and fix any issues + +**Scenario B: Codebase Refactoring** +User request: "I need to update all instances of the method 'fetchData' to +'retrieveInformation' throughout the entire project" + +Response approach: Let me first analyze your codebase to identify all locations +where 'fetchData' appears. + +*After code analysis* + +I've located 12 occurrences of 'fetchData' across 6 files in your project. +I'll create a systematic plan to update these references. + +*Organized task list includes specific file-by-file updates* + +**Scenario C: Multi-feature Development** +User request: "Set up a blog platform with these components: admin dashboard, +article management, comment system, and user profiles." + +Response approach: I'll help build your blog platform. Let me break down these +components into manageable implementation phases. + +*Creates structured plan with each feature decomposed into specific development tasks* + +## Counter-examples - Direct Implementation Preferred + +**Counter-example A: Simple inquiry** +User request: "What's the syntax for a for loop in JavaScript?" + +Response approach: Here's how to write a for loop in JavaScript: + +```javascript +for (let i = 0; i < array.length; i++) { + console.log(array[i]); +} +``` + +*No task tracking needed - single informational response* + +**Counter-example B: Minor modification** +User request: "Please add a docstring to the processPayment function explaining +its parameters." + +Response approach: I'll add a docstring to explain the processPayment function. + +*Direct edit without task tracking for simple modification* + +## Status Management and Workflow + +1. **Status Values**: Track work using these states: + - todo: Not yet initiated + - in_progress: Currently active (maintain single focus) + - done: Successfully completed + +2. **Workflow Practices**: + - Update status dynamically as work progresses + - Mark completion immediately upon task finish + - Limit active work to ONE task at any given time + - Complete current activities before initiating new ones + - Remove obsolete tasks from tracking entirely + +3. **Completion Criteria**: + - Mark tasks as done only when fully achieved + - Keep status as in_progress if errors, blocks, or partial completion exist + - Create new tasks for discovered issues or dependencies + - Never mark done when: + - Test suites are failing + - Implementation remains incomplete + - Unresolved errors persist + - Required resources are unavailable + +4. **Task Organization**: + - Write precise, actionable descriptions + - Decompose complex work into manageable units + - Use descriptive, clear naming conventions + +When uncertain, favor using this tool. Proactive task management demonstrates +systematic approach and ensures comprehensive requirement fulfillment. +""" + +_DETAILED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format * State is persistent across command calls and discussions with the user -* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep +* If `path` is a text file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep +* The following binary file extensions can be viewed in Markdown format: [".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".flac", ".pdf", ".docx"]. IT DOES NOT HANDLE IMAGES. * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` -Notes for using the `str_replace` command: -* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! -* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique -* The `new_str` parameter should contain the edited lines that should replace the `old_str` -Parameters: - 1. command (string, required): The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. - 2. path (string, required): Absolute path to file or directory, e.g. `/testbed/file.py` or `/testbed`. - 3. file_text (string, optional): Required parameter of `create` command, with the content of the file to be created. - 4. old_str (string, optional): Required parameter of `str_replace` command containing the string in `path` to replace. - 5. new_str (string, optional): Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert. - 6. insert_line (integer, optional): Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. - 7. view_range (array, optional): Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. -–– END FUNCTION #3 –– +* The `undo_edit` command will revert the last edit made to the file at `path` +* This tool can be used for creating and editing files in plain-text format. -–– BEGIN FUNCTION #4: submit –– -Description: -Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task. -No parameters are required for this function. -–– END FUNCTION #4 –– -You can execute actions in two primary ways: -1. By writing executable code blocks (CodeAct format): -```bash - -``` -or -```python - -``` +Before using this tool: +1. Use the view tool to understand the file's contents and context +2. Verify the directory path is correct (only applicable when creating new files): + - Use the view tool to verify the parent directory exists and is the correct location -2. By calling functions using the following XML format: - -value_1 - -value_2 - - +When making edits: + - Ensure the edit results in idiomatic, correct code + - Do not leave the code in a broken state + - Always use absolute file paths (starting with /) - -Reminder: -- Each response must include both reasoning (as natural text) and an action (either an executable code block or a function call). -- Only execute one action at a time. -- When the task is complete and verified, call submit (or ``). - +CRITICAL REQUIREMENTS FOR USING THIS TOOL: + +1. EXACT MATCHING: The `old_str` parameter must match EXACTLY one or more consecutive lines from the file, including all whitespace and indentation. The tool will fail if `old_str` matches multiple locations or doesn't match exactly with the file content. + +2. UNIQUENESS: The `old_str` must uniquely identify a single instance in the file: + - Include sufficient context before and after the change point (3-5 lines recommended) + - If not unique, the replacement will not be performed + +3. REPLACEMENT: The `new_str` parameter should contain the edited lines that replace the `old_str`. Both strings must be different. + +Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each. """ + +def get_openhands_tools( + max_timeout: int | None = None, + workspace_mount_path_in_sandbox: str = "/workspace", + enable_think: bool = True, + enable_task_tracker: bool = True, +) -> list[dict[str, Any]]: + """Returns the OpenHands CodeActAgent tool schemas matching MLPerf v6.1 rules.""" + if max_timeout is None: + max_timeout = int(os.getenv("COMMAND_EXEC_TIMEOUT", "60")) + + tools: list[dict[str, Any]] = [ + { + "type": "function", + "function": { + "name": "execute_bash", + "description": _DETAILED_BASH_DESCRIPTION, + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": ( + "The bash command to execute. Can be empty string" + " to view additional logs when previous exit code" + " is `-1`. Can be `C-c` (Ctrl+C) to interrupt the" + " currently running process. Note: You can only" + " execute one bash command at a time. If you need" + " to run multiple commands sequentially, you can" + " use `&&` or `;` to chain them together." + ), + }, + "is_input": { + "type": "string", + "description": ( + "If True, the command is an input to the running" + " process. If False, the command is a bash" + " command to be executed in the terminal. Default" + " is False." + ), + "enum": ["true", "false"], + }, + "timeout": { + "type": "number", + "description": ( + "Optional. Sets a hard timeout in seconds for the" + " command execution. If not provided, the command" + " will use the default soft timeout behavior. Max" + f" timeout allowed is {max_timeout} seconds." + ), + }, + "security_risk": { + "type": "string", + "description": _SECURITY_RISK_DESC, + "enum": list(_RISK_LEVELS), + }, + }, + "required": ["command"], + }, + }, + }, + ] + if enable_think: + tools.append({ + "type": "function", + "function": { + "name": "think", + "description": _THINK_DESCRIPTION, + "parameters": { + "type": "object", + "properties": { + "thought": { + "type": "string", + "description": "The thought to log.", + }, + }, + "required": ["thought"], + }, + }, + }) + tools.append({ + "type": "function", + "function": { + "name": "finish", + "description": _FINISH_DESCRIPTION, + "parameters": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "Final message to send to the user", + }, + }, + }, + }, + }) + if enable_task_tracker: + tools.append({ + "type": "function", + "function": { + "name": "task_tracker", + "description": _DETAILED_TASK_TRACKER_DESCRIPTION, + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": ["view", "plan"], + "description": ( + "The command to execute. `view` shows the current" + " task list. `plan` creates or updates the task" + " list based on provided requirements and progress." + " Always `view` the current list before making" + " changes." + ), + }, + "task_list": { + "type": "array", + "description": ( + "The full task list. Required parameter of `plan`" + " command." + ), + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique task identifier", + }, + "title": { + "type": "string", + "description": "Brief task description", + }, + "status": { + "type": "string", + "description": "Current task status", + "enum": ["todo", "in_progress", "done"], + }, + "notes": { + "type": "string", + "description": ( + "Optional additional context or details" + ), + }, + }, + "required": ["title", "status", "id"], + "additionalProperties": False, + }, + }, + }, + "required": ["command"], + "additionalProperties": False, + }, + }, + }) + tools.append({ + "type": "function", + "function": { + "name": "str_replace_editor", + "description": _DETAILED_STR_REPLACE_EDITOR_DESCRIPTION, + "parameters": { + "type": "object", + "properties": { + "command": { + "description": ( + "The commands to run. Allowed options are: `view`," + " `create`, `str_replace`, `insert`, `undo_edit`." + ), + "enum": [ + "view", + "create", + "str_replace", + "insert", + "undo_edit", + ], + "type": "string", + }, + "path": { + "description": ( + "Absolute path to file or directory, e.g." + f" `{workspace_mount_path_in_sandbox}/file.py` or" + f" `{workspace_mount_path_in_sandbox}`." + ), + "type": "string", + }, + "file_text": { + "description": ( + "Required parameter of `create` command, with the" + " content of the file to be created." + ), + "type": "string", + }, + "old_str": { + "description": ( + "Required parameter of `str_replace` command" + " containing the string in `path` to replace." + ), + "type": "string", + }, + "new_str": { + "description": ( + "Optional parameter of `str_replace` command" + " containing the new string (if not given, no string" + " will be added). Required parameter of `insert`" + " command containing the string to insert." + ), + "type": "string", + }, + "insert_line": { + "description": ( + "Required parameter of `insert` command. The" + " `new_str` will be inserted AFTER the line" + " `insert_line` of `path`." + ), + "type": "integer", + }, + "view_range": { + "description": ( + "Optional parameter of `view` command when `path`" + " points to a file. If none is given, the full file" + " is shown. If provided, the file will be shown in" + " the indicated line number range, e.g. [11, 12] will" + " show lines 11 and 12. Indexing at 1 to start." + " Setting `[start_line, -1]` shows all lines from" + " `start_line` to the end of the file." + ), + "items": {"type": "integer"}, + "type": "array", + }, + "security_risk": { + "type": "string", + "description": _SECURITY_RISK_DESC, + "enum": list(_RISK_LEVELS), + }, + }, + "required": ["command", "path"], + }, + }, + }) + return tools + + +OPENHANDS_TOOLS = get_openhands_tools() + +OPENHANDS_CODEACT_SYSTEM_PROMPT = """You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks. + + +Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed. +* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question. + + + +* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once. +* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations. + + + +* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it. +* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename. +* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times. +* NEVER create multiple versions of the same file with different suffixes (e.g., file_test.py, file_fix.py, file_simple.py). Instead: + - Always modify the original file directly when making changes + - If you need to create a temporary file for testing, delete it once you've confirmed your solution works + - If you decide a file you created is no longer useful, delete it instead of creating a new version +* Do NOT include documentation files explaining your changes in version control unless the user explicitly requests it +* When reproducing bugs or implementing fixes, use a single file rather than creating multiple files with different versions + + + +* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself. +* When implementing solutions, focus on making the minimal changes needed to solve the problem. +* Before implementing any changes, first thoroughly understand the codebase through exploration. +* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate. +* Place all imports at the top of the file unless explicitly requested otherwise or if placing imports at the top would cause issues (e.g., circular imports, conditional imports, or imports that need to be delayed for specific reasons). +* If working in a git repo, before you commit code create a .gitignore file if one doesn't exist. And if there are existing files that should not be included then update the .gitignore file as appropriate. + + + +* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so. +* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. +* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. +* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. + + + +* **Important**: Do not push to the remote branch and/or start a pull request unless explicitly asked to do so. +* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise. +* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue. +* When updating a PR, preserve the original PR title and purpose, updating description only when necessary. + + + +1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions +2. ANALYSIS: Consider multiple approaches and select the most promising one +3. TESTING: + * For bug fixes: Create tests to verify issues before implementing fixes + * For new features: Consider test-driven development when appropriate + * Do NOT write tests for documentation changes, README updates, configuration files, or other non-functionality changes + * If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure + * If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies +4. IMPLEMENTATION: + * Make focused, minimal changes to address the problem + * Always modify existing files directly rather than creating new versions with different suffixes + * If you create temporary files for testing, delete them after confirming your solution works +5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests. + + + +* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect. +* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing. + + + +# 🔐 Security Risk Policy +When using tools that support the security_risk parameter, assess the safety risk of your actions: + + +- **LOW**: Read-only actions inside sandbox. + - Inspecting container files, calculations, viewing docs. +- **MEDIUM**: Container-scoped edits and installs. + - Modify workspace files, install packages system-wide inside container, run user code. +- **HIGH**: Data exfiltration or privilege breaks. + - Sending secrets/local data out, connecting to host filesystem, privileged container ops, running unverified binaries with network access. + + + +**Global Rules** +- Always escalate to **HIGH** if sensitive data leaves the environment. + + + +* When interacting with external services like GitHub, GitLab, or Bitbucket, use their respective APIs instead of browser-based interactions whenever possible. +* Only resort to browser-based interactions with these services if specifically requested by the user or if the required operation cannot be performed via API. + + + +* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again. +* If you encounter missing dependencies: + 1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.) + 2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.) + 3. Only install individual packages directly if no dependency files are found or if only specific packages are needed +* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible. + + + +* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken: + 1. Step back and reflect on 5-7 different possible sources of the problem + 2. Assess the likelihood of each possible cause + 3. Methodically address the most likely causes, starting with the highest probability + 4. Document your reasoning process +* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding. + + + +* When explaining changes or solutions to the user: + - Include explanations in your conversation responses rather than creating separate documentation files + - If you need to create documentation files for reference, do NOT include them in version control unless explicitly requested + - Never create multiple versions of documentation files with different suffixes +* If the user asks for documentation: + - Confirm whether they want it as a separate file or just in the conversation + - Ask if they want documentation files to be included in version control + + + +* When terminating processes: + - Do NOT use general keywords with commands like `pkill -f server` or `pkill -f python` as this might accidentally kill other important servers or processes + - Always use specific keywords that uniquely identify the target process + - Prefer using `ps aux` to find the exact process ID (PID) first, then kill that specific PID + - When possible, use more targeted approaches like finding the PID from a pidfile or using application-specific shutdown commands +""" + + +def format_qwen_tools_system_prompt( + system_prompt: str = OPENHANDS_CODEACT_SYSTEM_PROMPT, + tools: list[dict[str, Any]] | None = None, +) -> str: + """Renders the Qwen3/Qwen3.5 native tool-calling system prompt.""" + if tools is None: + tools = get_openhands_tools() + tools_json = "\n".join(json.dumps(tool, ensure_ascii=False) for tool in tools) + return ( + "# Tools\n\n" + "You have access to the following functions:\n\n" + "\n" + f"{tools_json}\n" + "\n\n" + "If you choose to call a function ONLY reply in the following format" + " with NO suffix:\n\n" + "\n" + "\n" + "\n" + "value_1\n" + "\n" + "\n" + "This is the value for the second parameter\n" + "that can span\n" + "multiple lines\n" + "\n" + "\n" + "\n\n" + "\n" + "Reminder:\n" + "- Function calls MUST follow the specified format: an inner" + " block must be nested within" + " XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in" + " natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like" + " normal with your current knowledge and do not tell the user about" + " function calls\n" + "\n\n" + f"{system_prompt.strip()}" + ) + + +OPENHANDS_SYSTEM_PROMPT = format_qwen_tools_system_prompt() + +OPENHANDS_FAKE_USER_RESPONSE = ( + "Please continue working on the task on whatever approach you think is" + " suitable.\n" + "When you think you have solved the question, please use the finish tool" + " and include your final answer in the message parameter of the finish" + " tool.\n" + "IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP." +) + # ============================================================================== # Agent User Prompts # ============================================================================== @@ -427,6 +1005,108 @@ def get_agent_name(scaffold: str | None = None) -> str: """ +OPENHANDS_USER_PROMPT = """ +/testbed + + + +I've uploaded a python code repository in the directory /testbed. Consider the following issue description: + + +{problem_statement} + + +Can you help me implement the necessary changes to the repository so that the requirements specified in the are met? +I've already taken care of all changes to any of the test files described in the . This means you DON'T have to modify the testing logic or any of the tests in any way! +Also the development environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages. +Your task is to make the minimal changes to non-test files in the /testbed directory to ensure the is satisfied. + +Follow these phases to resolve the issue: + +Phase 1. READING: read the problem and reword it in clearer terms + 1.1 If there are code or config snippets. Express in words any best practices or conventions in them. + 1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details. + 1.3 Explain the problem in clear terms. + 1.4 Enumerate the steps to reproduce the problem. + 1.5 Hightlight any best practices to take into account when testing and fixing the issue + +Phase 2. RUNNING: install and run the tests on the repository + 2.1 Follow the readme + 2.2 Install the environment and anything needed + 2.2 Iterate and figure out how to run the tests + +Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions + 3.1 Use `grep` to search for relevant methods, classes, keywords and error messages. + 3.2 Identify all files related to the problem statement. + 3.3 Propose the methods and files to fix the issue and explain why. + 3.4 From the possible file locations, select the most likely location to fix the issue. + +Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue. + 4.1 Look at existing test files in the repository to understand the test format/structure. + 4.2 Create a minimal reproduction script that reproduces the located issue. + 4.3 Run the reproduction script to confirm you are reproducing the issue. + 4.4 Adjust the reproduction script as necessary. + +Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it + 5.1 State clearly what the problem is. + 5.2 State clearly where the problem is located. + 5.3 State clearly how the test reproduces the issue. + 5.4 State clearly the best practices to take into account in the fix. + 5.5 State clearly how to fix the problem. + +Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution. + 6.1 Make minimal, focused changes to fix the issue. + +Phase 7. VERIFICATION: Test your implementation thoroughly. + 7.1 Run your reproduction script to verify the fix works. + 7.2 Add edge cases to your test script to ensure comprehensive coverage. + 7.3 Run existing tests related to the modified code to ensure you haven't broken anything. + +8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit . + 8.1 Ensure you've fully addressed all requirements. + 8.2 Run any tests in the repository related to: + 8.2.1 The issue you are fixing + 8.2.2 The files you modified + 8.2.3 The functions you changed + 8.3 If any tests fail, revise your implementation until all tests pass + +Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.""" + + +def format_openhands_user_prompt( + problem_statement: str, + workspace_path: str = "/testbed", + repo_language: str = "python", + base_commit: str = "", +) -> str: + """Renders the OpenHands swe_default.j2 user prompt.""" + prompt = OPENHANDS_USER_PROMPT + if workspace_path != "/testbed": + prompt = prompt.replace( + "\n/testbed\n", + f"\n{workspace_path}\n", + ) + prompt = prompt.replace( + "in the directory /testbed.", + f"in the directory {workspace_path}.", + ) + prompt = prompt.replace( + "in the /testbed directory", + f"in the {workspace_path} directory", + ) + if repo_language != "python": + prompt = prompt.replace( + "I've uploaded a python code repository", + f"I've uploaded a {repo_language} code repository", + ) + if base_commit: + prompt = prompt.replace( + "compare your changes with the base commit .", + f"compare your changes with the base commit {base_commit}.", + ) + return prompt.replace("{problem_statement}", str(problem_statement)) + + def get_system_prompt( scaffold: str = "r2egym", use_fn_calling: bool = False, @@ -446,6 +1126,8 @@ def get_user_prompt_template( """Get user prompt template for the given scaffold and function calling mode.""" if scaffold == "sweagent": return SWEAGENT_USER_PROMPT + elif scaffold in OPENHANDS_SCAFFOLDS: + return OPENHANDS_USER_PROMPT return SWE_USER_PROMPT_FN_CALL if use_fn_calling else SWE_USER_PROMPT diff --git a/tests/examples/template_test.py b/tests/examples/template_test.py index 40e3d5b7c0..bbaa6351dd 100644 --- a/tests/examples/template_test.py +++ b/tests/examples/template_test.py @@ -37,6 +37,7 @@ def test_prompt_constants_exist(): assert template.SWE_USER_PROMPT_FN_CALL assert template.SWE_USER_PROMPT assert template.SWEAGENT_USER_PROMPT + assert template.OPENHANDS_USER_PROMPT def test_get_system_prompt(): @@ -83,7 +84,11 @@ def test_get_user_prompt_template(): ) assert ( template.get_user_prompt_template("openhands", use_fn_calling=False) - == template.SWE_USER_PROMPT + == template.OPENHANDS_USER_PROMPT + ) + assert ( + template.get_user_prompt_template("openhands", use_fn_calling=True) + == template.OPENHANDS_USER_PROMPT ) diff --git a/tests/rl/agentic/parser/chat_template_parser/chat_template_parser_test.py b/tests/rl/agentic/parser/chat_template_parser/chat_template_parser_test.py index 33b3bcd45e..afbf94a095 100644 --- a/tests/rl/agentic/parser/chat_template_parser/chat_template_parser_test.py +++ b/tests/rl/agentic/parser/chat_template_parser/chat_template_parser_test.py @@ -101,6 +101,33 @@ def test_parse_with_disable_thinking(self): ) self.assertEqual(result, expected) + def test_parse_preserves_tool_call_tags_in_system_and_assistant(self): + p = parser.QwenChatTemplateParser(self.mock_tokenizer) + messages = [ + { + 'role': 'system', + 'content': ( + 'Use\n\n\n' + '\npwd\n\n' + '\n' + ), + }, + {'role': 'user', 'content': 'Run pwd'}, + { + 'role': 'assistant', + 'content': ( + '\nChecking pwd.\n\n\n' + '\n\n' + '\npwd\n\n' + '\n' + ), + }, + ] + result = p.parse(messages, is_first_msg=True) + self.assertIn('\n\n', result) + self.assertIn('\n<|im_end|>', result) + + class LlamaChatTemplateParserTest(absltest.TestCase): diff --git a/tunix/experimental/examples/deepswe_dist/eval_deepswe.py b/tunix/experimental/examples/deepswe_dist/eval_deepswe.py index 0aaef6e373..6be325c5ba 100644 --- a/tunix/experimental/examples/deepswe_dist/eval_deepswe.py +++ b/tunix/experimental/examples/deepswe_dist/eval_deepswe.py @@ -114,9 +114,9 @@ def parse_args(argv=None): ) p.add_argument("--vllm_max_num_seqs", type=int, default=128) p.add_argument("--vllm_max_num_batched_tokens", type=int, default=32768) - p.add_argument("--timeout", type=float, default=3600) - p.add_argument("--reward_timeout", type=int, default=1800) - p.add_argument("--step_timeout", type=int, default=600) + p.add_argument("--timeout", type=float, default=1800) + p.add_argument("--reward_timeout", type=int, default=60) + p.add_argument("--step_timeout", type=int, default=60) p.add_argument("--startup_timeout", type=float, default=7200) p.add_argument("--temperature", type=float, default=0.7) p.add_argument("--top_p", type=float, default=1.0) diff --git a/tunix/experimental/examples/deepswe_dist/eval_worker.py b/tunix/experimental/examples/deepswe_dist/eval_worker.py index 7dbc698add..57290b3c55 100644 --- a/tunix/experimental/examples/deepswe_dist/eval_worker.py +++ b/tunix/experimental/examples/deepswe_dist/eval_worker.py @@ -243,6 +243,13 @@ def create_worker(a): except Exception: pass + from examples.deepswe import template as deepswe_template + + sampling_kwargs = {"skip_special_tokens": False} + if a.scaffold not in deepswe_template.OPENHANDS_SCAFFOLDS: + sampling_kwargs["stop"] = [""] + sampling_kwargs["include_stop_str_in_output"] = True + config = vllm_sampler.VllmConfig( server_mode=True, mesh=mesh, @@ -255,11 +262,7 @@ def create_worker(a): additional_config=additional_config, engine_kwargs=engine_kwargs, eos_tokens=eos_ids, - sampling_kwargs={ - "stop": [""], - "include_stop_str_in_output": True, - "skip_special_tokens": False, - }, + sampling_kwargs=sampling_kwargs, ) sampler = inprocess_vllm_sampler_adapter.InprocessVllmSamplerAdapter( server_id="deepswe-eval", diff --git a/tunix/experimental/examples/deepswe_dist/run_deepswe_dist.py b/tunix/experimental/examples/deepswe_dist/run_deepswe_dist.py index 3a14d3d257..37fb73d865 100644 --- a/tunix/experimental/examples/deepswe_dist/run_deepswe_dist.py +++ b/tunix/experimental/examples/deepswe_dist/run_deepswe_dist.py @@ -341,13 +341,23 @@ def _parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument( "--shuffle", action=argparse.BooleanOptionalAction, default=True ) - parser.add_argument("--max_turns", type=int, default=50) - parser.add_argument("--step_timeout_secs", type=int, default=30 * 60) - parser.add_argument("--reward_timeout_secs", type=int, default=30 * 60) + parser.add_argument( + "--max_turns", type=int, default=int(os.getenv("MAX_TURNS", "30")) + ) + parser.add_argument( + "--step_timeout_secs", + type=int, + default=int(os.getenv("STEP_TIMEOUT_SECS", "60")), + ) + parser.add_argument( + "--reward_timeout_secs", + type=int, + default=int(os.getenv("REWARD_TIMEOUT_SECS", "60")), + ) parser.add_argument( "--episode_timeout_secs", type=int, - default=int(os.getenv("EPISODE_TIMEOUT_SECS", "5400")), + default=int(os.getenv("EPISODE_TIMEOUT_SECS", "1800")), help="Maximum episode duration in seconds before timeout termination.", ) parser.add_argument("--env_backend", type=str, default="kubernetes") diff --git a/tunix/experimental/examples/recipes/mlperf_base.sh b/tunix/experimental/examples/recipes/mlperf_base.sh index 49308cf954..34c5bfca0f 100755 --- a/tunix/experimental/examples/recipes/mlperf_base.sh +++ b/tunix/experimental/examples/recipes/mlperf_base.sh @@ -327,8 +327,8 @@ export STEP_TIMEOUT_SECS="${STEP_TIMEOUT_SECS:-60}" export REWARD_TIMEOUT_SECS="${REWARD_TIMEOUT_SECS:-60}" export FLUSH_EVERY_N_STEPS=1 export MAX_TURNS=30 -export MAX_PROMPT_LENGTH="${MAX_PROMPT_LENGTH:-4096}" -export MAX_RESPONSE_LENGTH="${MAX_RESPONSE_LENGTH:-61440}" +export MAX_PROMPT_LENGTH="${MAX_PROMPT_LENGTH:-8192}" +export MAX_RESPONSE_LENGTH="${MAX_RESPONSE_LENGTH:-57344}" # ============================================================================== # Execution Dispatch diff --git a/tunix/rl/agentic/parser/chat_template_parser/parser.py b/tunix/rl/agentic/parser/chat_template_parser/parser.py index 7c24cf560d..f3d6b65fb1 100644 --- a/tunix/rl/agentic/parser/chat_template_parser/parser.py +++ b/tunix/rl/agentic/parser/chat_template_parser/parser.py @@ -56,11 +56,20 @@ def __init__(self, tokenizer, enable_thinking: bool = True): self.generation_prompt = self._init_generation_prompt() # message_separator is a per-turn formatting hint (e.g. "\n"), not a # control token; including it would strip every newline from message - # content. + # content. Similarly, tool_start_token and tool_end_token (e.g. + # "\n\n" and "\n") legitimately appear inside + # tool-use system prompts and assistant tool-call turns. self._tokens_to_sanitize = { v for k, v in dataclasses.asdict(self.tokens).items() - if k != "message_separator" and isinstance(v, str) and v + if k + not in ( + "message_separator", + "tool_start_token", + "tool_end_token", + ) + and isinstance(v, str) + and v } @abstractmethod