Skip to content

Commit f7e3d8c

Browse files
committed
WIP: refactoring replay code into its own set of files
1 parent bb77b47 commit f7e3d8c

File tree

7 files changed

+191
-141
lines changed

7 files changed

+191
-141
lines changed

openhands/agenthub/codeact_agent/codeact_agent.py

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@
3636
from openhands.events.observation.error import ErrorObservation
3737
from openhands.events.observation.observation import Observation
3838
from openhands.events.observation.replay import (
39-
ReplayPhaseUpdateObservation,
40-
ReplayToolCmdOutputObservation,
39+
ReplayObservation,
4140
)
42-
from openhands.events.replay import replay_enhance_action
4341
from openhands.events.serialization.event import truncate_content
4442
from openhands.llm.llm import LLM
43+
from openhands.replay.replay_commands import replay_enhance_action
44+
from openhands.replay.replay_state_machine import (
45+
get_replay_observation_message,
46+
)
4547
from openhands.runtime.plugins import (
4648
AgentSkillsRequirement,
4749
JupyterRequirement,
@@ -253,38 +255,8 @@ def get_observation_message(
253255
)
254256
text += f'\n[Command finished with exit code {obs.exit_code}]'
255257
message = Message(role='user', content=[TextContent(text=text)])
256-
elif isinstance(obs, ReplayToolCmdOutputObservation):
257-
# if it doesn't have tool call metadata, it was triggered by a user action
258-
if obs.tool_call_metadata is None:
259-
text = truncate_content(
260-
f'\nObserved result of replay command executed by user:\n{obs.content}',
261-
max_message_chars,
262-
)
263-
else:
264-
text = obs.content
265-
message = Message(role='user', content=[TextContent(text=text)])
266-
elif isinstance(obs, ReplayPhaseUpdateObservation):
267-
# NOTE: The phase change itself is handled in AgentController.
268-
new_phase = obs.new_phase
269-
if new_phase == ReplayDebuggingPhase.Edit:
270-
# Tell the agent to stop analyzing and start editing:
271-
text = """
272-
You have concluded the analysis.
273-
274-
IMPORTANT: NOW review, then implement the hypothesized changes using tools. The code is available in the workspace. Start by answering these questions:
275-
1. What is the goal of the investigation according to the initial prompt and initial analysis? IMPORTANT. PAY ATTENTION TO THIS. THIS IS THE ENTRY POINT OF EVERYTHING.
276-
2. Given (1), is the hypothesis's `problem` description correct? Does it match the goal of the investigation?
277-
3. Do the `editSuggestions` actually address the issue?
278-
4. Rephrase the hypothesis so that it is consistent and correct.
279-
280-
IMPORTANT: Don't stop. Keep working.
281-
IMPORTANT: Don't stop. Keep working.
282-
"""
283-
message = Message(role='user', content=[TextContent(text=text)])
284-
else:
285-
raise NotImplementedError(
286-
f'Unhandled ReplayPhaseUpdateAction: {new_phase}'
287-
)
258+
elif isinstance(obs, ReplayObservation):
259+
message = get_replay_observation_message(obs, max_message_chars)
288260
elif isinstance(obs, IPythonRunCellObservation):
289261
text = obs.content
290262
# replace base64 images with a placeholder
@@ -388,7 +360,7 @@ def step(self, state: State) -> Action:
388360
return AgentFinishAction()
389361

390362
if self.config.codeact_enable_replay:
391-
# Replay enhancement.
363+
# Check for whether we should enhance the prompt.
392364
enhance_action = replay_enhance_action(state, self.config.is_workspace_repo)
393365
if enhance_action:
394366
logger.info('[REPLAY] Enhancing prompt for Replay recording...')

openhands/controller/agent_controller.py

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@
4949
)
5050
from openhands.events.observation.replay import (
5151
ReplayInternalCmdOutputObservation,
52-
ReplayPhaseUpdateObservation,
52+
ReplayObservation,
5353
)
54-
from openhands.events.replay import handle_replay_internal_observation
5554
from openhands.events.serialization.event import truncate_content
5655
from openhands.llm.llm import LLM
56+
from openhands.replay.replay_state_machine import on_replay_observation
5757
from openhands.utils.shutdown_listener import should_continue
5858

5959
# note: RESUME is only available on web GUI
@@ -297,28 +297,8 @@ async def _handle_observation(self, observation: Observation) -> None:
297297

298298
if self._pending_action and self._pending_action.id == observation.cause:
299299
self._pending_action = None
300-
if isinstance(observation, ReplayInternalCmdOutputObservation):
301-
# NOTE: Currently, the only internal command is the initial-analysis command.
302-
analysis_tool_metadata = handle_replay_internal_observation(
303-
self.state, observation
304-
)
305-
if analysis_tool_metadata:
306-
# Start analysis phase
307-
self.state.replay_recording_id = analysis_tool_metadata[
308-
'recordingId'
309-
]
310-
self.state.replay_phase = ReplayDebuggingPhase.Analysis
311-
self.agent.replay_phase_changed(ReplayDebuggingPhase.Analysis)
312-
elif isinstance(observation, ReplayPhaseUpdateObservation):
313-
new_phase = observation.new_phase
314-
if self.state.replay_phase == new_phase:
315-
self.log(
316-
'warning',
317-
f'Unexpected ReplayPhaseUpdateAction. Already in phase. Observation:\n {repr(observation)}',
318-
)
319-
else:
320-
self.state.replay_phase = new_phase
321-
self.agent.replay_phase_changed(new_phase)
300+
if isinstance(observation, ReplayObservation):
301+
on_replay_observation(observation, self.state, self.agent)
322302

323303
if self.state.agent_state == AgentState.USER_CONFIRMED:
324304
await self.set_agent_state_to(AgentState.RUNNING)

openhands/events/observation/replay.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from abc import ABC
12
from dataclasses import dataclass
23

34
from openhands.core.schema import ObservationType
@@ -6,7 +7,12 @@
67

78

89
@dataclass
9-
class ReplayCmdOutputObservationBase(Observation):
10+
class ReplayObservation(Observation, ABC):
11+
pass
12+
13+
14+
@dataclass
15+
class ReplayCmdOutputObservationBase(ReplayObservation, ABC):
1016
"""This data class represents the output of a replay command."""
1117

1218
command_id: int
@@ -38,7 +44,7 @@ class ReplayToolCmdOutputObservation(ReplayCmdOutputObservationBase):
3844

3945

4046
@dataclass
41-
class ReplayPhaseUpdateObservation(Observation):
47+
class ReplayPhaseUpdateObservation(ReplayObservation):
4248
new_phase: ReplayDebuggingPhase
4349
observation: str = ObservationType.REPLAY_UPDATE_PHASE
4450

Lines changed: 12 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import json
22
import re
3-
from typing import Any, TypedDict, cast
3+
from typing import Any, cast
44

55
from openhands.controller.state.state import State
66
from openhands.core.logger import openhands_logger as logger
77
from openhands.events.action.action import Action
88
from openhands.events.action.message import MessageAction
99
from openhands.events.action.replay import ReplayInternalCmdRunAction
1010
from openhands.events.observation.replay import ReplayInternalCmdOutputObservation
11+
from openhands.replay.replay_prompts import replay_prompt_phase_analysis
12+
from openhands.replay.replay_types import AnalysisToolMetadata, AnnotateResult
1113

1214

1315
def scan_recording_id(issue: str) -> str | None:
@@ -24,7 +26,7 @@ def scan_recording_id(issue: str) -> str | None:
2426

2527

2628
# Produce the command string for the `annotate-execution-points` command.
27-
def command_annotate_execution_points(
29+
def start_initial_analysis(
2830
thought: str, is_workspace_repo: bool
2931
) -> ReplayInternalCmdRunAction:
3032
command_input: dict[str, Any] = dict()
@@ -57,30 +59,12 @@ def replay_enhance_action(state: State, is_workspace_repo: bool) -> Action | Non
5759
)
5860
state.extra_data['replay_enhance_prompt_id'] = latest_user_message.id
5961
logger.info('[REPLAY] stored latest_user_message id in state')
60-
return command_annotate_execution_points(
62+
return start_initial_analysis(
6163
latest_user_message.content, is_workspace_repo
6264
)
6365
return None
6466

6567

66-
class AnnotatedLocation(TypedDict, total=False):
67-
filePath: str
68-
line: int
69-
70-
71-
class AnalysisToolMetadata(TypedDict, total=False):
72-
recordingId: str
73-
74-
75-
class AnnotateResult(TypedDict, total=False):
76-
point: str
77-
commentText: str | None
78-
annotatedRepo: str | None
79-
annotatedLocations: list[AnnotatedLocation] | None
80-
pointLocation: str | None
81-
metadata: AnalysisToolMetadata | None
82-
83-
8468
def safe_parse_json(text: str) -> dict[str, Any] | None:
8569
try:
8670
return json.loads(text)
@@ -97,15 +81,7 @@ def split_metadata(result):
9781
return metadata, data
9882

9983

100-
def enhance_prompt(user_message: MessageAction, prefix: str, suffix: str | None = None):
101-
if prefix != '':
102-
user_message.content = f'{prefix}\n\n{user_message.content}'
103-
if suffix is not None:
104-
user_message.content = f'{user_message.content}\n\n{suffix}'
105-
logger.info(f'[REPLAY] Enhanced user prompt:\n{user_message.content}')
106-
107-
108-
def handle_replay_internal_observation(
84+
def handle_replay_internal_command_observation(
10985
state: State, observation: ReplayInternalCmdOutputObservation
11086
) -> AnalysisToolMetadata | None:
11187
"""
@@ -126,63 +102,20 @@ def handle_replay_internal_observation(
126102
assert user_message
127103
state.extra_data['replay_enhance_observed'] = True
128104

105+
# Deserialize stringified result.
129106
result: AnnotateResult = cast(
130107
AnnotateResult, safe_parse_json(observation.content)
131108
)
132109

133-
# Determine what initial-analysis did:
110+
# Get metadata and enhance prompt.
134111
if result and 'metadata' in result:
135-
# New workflow: initial-analysis provided the metadata to allow tool use.
136-
metadata, data = split_metadata(result)
137-
prefix = ''
138-
suffix = """
139-
# Instructions
140-
0. Take a look at below `Initial Analysis`, based on a recorded trace of the bug. Pay special attention to `IMPORTANT_NOTES`.
141-
1. State the main problem statement. It MUST address `IMPORTANT_NOTES`. It must make sure that the application won't crash. It must fix the issue.
142-
2. Propose a plan to fix or investigate with multiple options in order of priority.
143-
3. Then use the `inspect-*` tools to investigate.
144-
4. Once found, `submit-hypothesis`.
145-
146-
147-
# Initial Analysis
148-
""" + json.dumps(data, indent=2)
149-
enhance_prompt(
150-
user_message,
151-
prefix,
152-
suffix,
153-
)
112+
# initial-analysis provides metadata needed for tool use.
113+
metadata, command_result = split_metadata(result)
114+
replay_prompt_phase_analysis(command_result, user_message)
154115
return metadata
155-
elif result and result.get('annotatedRepo'):
156-
# Old workflow: initial-analysis left hints in form of source code annotations.
157-
annotated_repo_path = result.get('annotatedRepo', '')
158-
comment_text = result.get('commentText', '')
159-
react_component_name = result.get('reactComponentName', '')
160-
console_error = result.get('consoleError', '')
161-
# start_location = result.get('startLocation', '')
162-
start_name = result.get('startName', '')
163-
164-
# TODO: Move this to a prompt template file.
165-
if comment_text:
166-
if react_component_name:
167-
prefix = f'There is a change needed to the {react_component_name} component.\n'
168-
else:
169-
prefix = f'There is a change needed in {annotated_repo_path}:\n'
170-
prefix += f'{comment_text}\n\n'
171-
elif console_error:
172-
prefix = f'There is a change needed in {annotated_repo_path} to fix a console error that has appeared unexpectedly:\n'
173-
prefix += f'{console_error}\n\n'
174-
175-
prefix += '<IMPORTANT>\n'
176-
prefix += 'Information about a reproduction of the problem is available in source comments.\n'
177-
prefix += 'You must search for these comments and use them to get a better understanding of the problem.\n'
178-
prefix += f'The first reproduction comment to search for is named {start_name}. Start your investigation there.\n'
179-
prefix += '</IMPORTANT>\n'
180-
181-
enhance_prompt(user_message, prefix)
182-
return None
183116
else:
184117
logger.warning(
185-
f'[REPLAY] Replay observation cannot be interpreted. Observed content: {str(observation.content)}'
118+
f'[REPLAY] Replay command result cannot be interpreted. Observed content: {str(observation.content)}'
186119
)
187120

188121
return None

openhands/replay/replay_prompts.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import json
2+
3+
from openhands.core.logger import openhands_logger as logger
4+
from openhands.events.action.message import MessageAction
5+
from openhands.replay.replay_types import AnnotateResult
6+
7+
8+
def enhance_prompt(user_message: MessageAction, prefix: str, suffix: str):
9+
if prefix != '':
10+
user_message.content = f'{prefix}\n\n{user_message.content}'
11+
if suffix != '':
12+
user_message.content = f'{user_message.content}\n\n{suffix}'
13+
logger.info(f'[REPLAY] Enhanced user prompt:\n{user_message.content}')
14+
15+
16+
def replay_prompt_phase_analysis(command_result: dict, user_message: MessageAction):
17+
prefix = ''
18+
suffix = """
19+
# Instructions
20+
0. Take a look at below `Initial Analysis`, based on a recorded trace of the bug. Pay special attention to `IMPORTANT_NOTES`.
21+
1. State the main problem statement. It MUST address `IMPORTANT_NOTES`. It must make sure that the application won't crash. It must fix the issue.
22+
2. Propose a plan to fix or investigate with multiple options in order of priority.
23+
3. Then use the `inspect-*` tools to investigate.
24+
4. Once found, `submit-hypothesis`.
25+
26+
27+
# Initial Analysis
28+
""" + json.dumps(command_result, indent=2)
29+
return enhance_prompt(user_message, prefix, suffix)
30+
31+
32+
def replay_prompt_phase_analysis_legacy(
33+
command_result: AnnotateResult, user_message: MessageAction
34+
):
35+
# Old workflow: initial-analysis left hints in form of source code annotations.
36+
annotated_repo_path = command_result.get('annotatedRepo', '')
37+
comment_text = command_result.get('commentText', '')
38+
react_component_name = command_result.get('reactComponentName', '')
39+
console_error = command_result.get('consoleError', '')
40+
# start_location = result.get('startLocation', '')
41+
start_name = command_result.get('startName', '')
42+
43+
# TODO: Move this to a prompt template file.
44+
if comment_text:
45+
if react_component_name:
46+
prefix = (
47+
f'There is a change needed to the {react_component_name} component.\n'
48+
)
49+
else:
50+
prefix = f'There is a change needed in {annotated_repo_path}:\n'
51+
prefix += f'{comment_text}\n\n'
52+
elif console_error:
53+
prefix = f'There is a change needed in {annotated_repo_path} to fix a console error that has appeared unexpectedly:\n'
54+
prefix += f'{console_error}\n\n'
55+
56+
prefix += '<IMPORTANT>\n'
57+
prefix += 'Information about a reproduction of the problem is available in source comments.\n'
58+
prefix += 'You must search for these comments and use them to get a better understanding of the problem.\n'
59+
prefix += f'The first reproduction comment to search for is named {start_name}. Start your investigation there.\n'
60+
prefix += '</IMPORTANT>\n'
61+
62+
suffix = ''
63+
64+
return enhance_prompt(user_message, prefix, suffix)
65+
66+
67+
def replay_prompt_phase_edit():
68+
# Tell the agent to stop analyzing and start editing:
69+
return """
70+
You have concluded the analysis.
71+
72+
IMPORTANT: NOW review, then implement the hypothesized changes using tools. The code is available in the workspace. Start by answering these questions:
73+
1. What is the goal of the investigation according to the initial prompt and initial analysis? IMPORTANT. PAY ATTENTION TO THIS. THIS IS THE ENTRY POINT OF EVERYTHING.
74+
2. Given (1), is the hypothesis's `problem` description correct? Does it match the goal of the investigation?
75+
3. Do the `editSuggestions` actually address the issue?
76+
4. Rephrase the hypothesis so that it is consistent and correct.
77+
"""

0 commit comments

Comments
 (0)