mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Add diff for edit observation and display in UI (#7014)
This commit is contained in:
@@ -257,6 +257,7 @@ export enum I18nKey {
|
||||
STATUS$ERROR_LLM_AUTHENTICATION = "STATUS$ERROR_LLM_AUTHENTICATION",
|
||||
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
|
||||
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
|
||||
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
@@ -279,6 +280,7 @@ export enum I18nKey {
|
||||
ACTION_MESSAGE$EDIT = "ACTION_MESSAGE$EDIT",
|
||||
ACTION_MESSAGE$WRITE = "ACTION_MESSAGE$WRITE",
|
||||
ACTION_MESSAGE$BROWSE = "ACTION_MESSAGE$BROWSE",
|
||||
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
|
||||
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
|
||||
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
|
||||
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
|
||||
@@ -306,11 +308,8 @@ export enum I18nKey {
|
||||
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
|
||||
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
|
||||
SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL = "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL",
|
||||
BUTTON$ENABLE_SOUND = "BUTTON$ENABLE_SOUND",
|
||||
BUTTON$DISABLE_SOUND = "BUTTON$DISABLE_SOUND",
|
||||
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
|
||||
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
|
||||
NOTIFICATION$SOUND_ENABLED = "NOTIFICATION$SOUND_ENABLED",
|
||||
NOTIFICATION$SOUND_DISABLED = "NOTIFICATION$SOUND_DISABLED",
|
||||
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
|
||||
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
|
||||
}
|
||||
|
||||
@@ -89,6 +89,16 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
);
|
||||
break;
|
||||
case "read":
|
||||
store.dispatch(
|
||||
addAssistantObservation({
|
||||
...baseObservation,
|
||||
observation,
|
||||
extras: {
|
||||
path: String(message.extras.path || ""),
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "edit":
|
||||
store.dispatch(
|
||||
addAssistantObservation({
|
||||
@@ -96,6 +106,7 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
observation,
|
||||
extras: {
|
||||
path: String(message.extras.path || ""),
|
||||
diff: String(message.extras.diff || ""),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -173,9 +173,14 @@ export const chatSlice = createSlice({
|
||||
causeMessage.content
|
||||
}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
|
||||
causeMessage.content = content; // Observation content includes the action
|
||||
} else if (observationID === "read" || observationID === "edit") {
|
||||
const { content } = observation.payload;
|
||||
causeMessage.content = `\`\`\`${observationID === "edit" ? "diff" : "python"}\n${content}\n\`\`\``; // Content is already truncated by the ACI
|
||||
} else if (observationID === "read") {
|
||||
causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI
|
||||
} else if (observationID === "edit") {
|
||||
if (causeMessage.success) {
|
||||
causeMessage.content = `\`\`\`diff\n${observation.payload.extras.diff}\n\`\`\``; // Content is already truncated by the ACI
|
||||
} else {
|
||||
causeMessage.content = observation.payload.content;
|
||||
}
|
||||
} else if (observationID === "browse") {
|
||||
let content = `**URL:** ${observation.payload.extras.url}\n`;
|
||||
if (observation.payload.extras.error) {
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface EditObservation extends OpenHandsObservationEvent<"edit"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
path: string;
|
||||
diff: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,12 @@ class FileEditObservation(Observation):
|
||||
new_content: str | None = None
|
||||
observation: str = ObservationType.EDIT
|
||||
impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
|
||||
_diff_cache: str | None = None # Cache for the diff visualization
|
||||
diff: str | None = (
|
||||
None # The raw diff between old and new content, used in OH_ACI mode
|
||||
)
|
||||
_diff_cache: str | None = (
|
||||
None # Cache for the diff visualization, used in LLM-based editing mode
|
||||
)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -126,7 +131,7 @@ class FileEditObservation(Observation):
|
||||
n_context_lines: int = 2,
|
||||
change_applied: bool = True,
|
||||
) -> str:
|
||||
"""Visualize the diff of the file edit.
|
||||
"""Visualize the diff of the file edit. Used in the LLM-based editing mode.
|
||||
|
||||
Instead of showing the diff line by line, this function shows each hunk
|
||||
of changes as a separate entity.
|
||||
|
||||
@@ -25,6 +25,7 @@ from fastapi.security import APIKeyHeader
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
from openhands_aci.editor.exceptions import ToolError
|
||||
from openhands_aci.editor.results import ToolResult
|
||||
from openhands_aci.utils.diff import get_diff
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
@@ -89,7 +90,7 @@ def _execute_file_editor(
|
||||
new_str: str | None = None,
|
||||
insert_line: int | None = None,
|
||||
enable_linting: bool = False,
|
||||
) -> str:
|
||||
) -> tuple[str, tuple[str | None, str | None]]:
|
||||
"""Execute file editor command and handle exceptions.
|
||||
|
||||
Args:
|
||||
@@ -104,7 +105,7 @@ def _execute_file_editor(
|
||||
enable_linting: Whether to enable linting
|
||||
|
||||
Returns:
|
||||
str: Result string from the editor operation
|
||||
tuple: A tuple containing the output string and a tuple of old and new file content
|
||||
"""
|
||||
result: ToolResult | None = None
|
||||
try:
|
||||
@@ -122,13 +123,13 @@ def _execute_file_editor(
|
||||
result = ToolResult(error=e.message)
|
||||
|
||||
if result.error:
|
||||
return f'ERROR:\n{result.error}'
|
||||
return f'ERROR:\n{result.error}', (None, None)
|
||||
|
||||
if not result.output:
|
||||
logger.warning(f'No output from file_editor for {path}')
|
||||
return ''
|
||||
return '', (None, None)
|
||||
|
||||
return result.output
|
||||
return result.output, (result.old_content, result.new_content)
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
@@ -316,7 +317,7 @@ class ActionExecutor:
|
||||
async def read(self, action: FileReadAction) -> Observation:
|
||||
assert self.bash_session is not None
|
||||
if action.impl_source == FileReadSource.OH_ACI:
|
||||
result_str = _execute_file_editor(
|
||||
result_str, _ = _execute_file_editor(
|
||||
self.file_editor,
|
||||
command='view',
|
||||
path=action.path,
|
||||
@@ -433,7 +434,7 @@ class ActionExecutor:
|
||||
|
||||
async def edit(self, action: FileEditAction) -> Observation:
|
||||
assert action.impl_source == FileEditSource.OH_ACI
|
||||
result_str = _execute_file_editor(
|
||||
result_str, (old_content, new_content) = _execute_file_editor(
|
||||
self.file_editor,
|
||||
command=action.command,
|
||||
path=action.path,
|
||||
@@ -450,6 +451,11 @@ class ActionExecutor:
|
||||
old_content=action.old_str,
|
||||
new_content=action.new_str,
|
||||
impl_source=FileEditSource.OH_ACI,
|
||||
diff=get_diff(
|
||||
old_contents=old_content or '',
|
||||
new_contents=new_content or '',
|
||||
filepath=action.path,
|
||||
),
|
||||
)
|
||||
|
||||
async def browse(self, action: BrowseURLAction) -> Observation:
|
||||
|
||||
@@ -160,6 +160,7 @@ def test_file_edit_observation_serialization():
|
||||
'old_content': None,
|
||||
'path': '',
|
||||
'prev_exist': False,
|
||||
'diff': None,
|
||||
},
|
||||
'message': 'I edited the file .',
|
||||
'content': '[Existing file /path/to/file.txt is edited with 1 changes.]',
|
||||
@@ -178,6 +179,7 @@ def test_file_edit_observation_new_file_serialization():
|
||||
'old_content': None,
|
||||
'path': '',
|
||||
'prev_exist': False,
|
||||
'diff': None,
|
||||
},
|
||||
'message': 'I edited the file .',
|
||||
}
|
||||
@@ -196,6 +198,7 @@ def test_file_edit_observation_oh_aci_serialization():
|
||||
'old_content': None,
|
||||
'path': '',
|
||||
'prev_exist': False,
|
||||
'diff': None,
|
||||
},
|
||||
'message': 'I edited the file .',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user