Add diff for edit observation and display in UI (#7014)

This commit is contained in:
Ryan H. Tran
2025-02-28 23:36:32 +07:00
committed by GitHub
parent 2b3c38d061
commit 32ee6a5a64
7 changed files with 46 additions and 16 deletions

View File

@@ -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",
}

View File

@@ -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 || ""),
},
}),
);

View File

@@ -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) {

View File

@@ -70,6 +70,7 @@ export interface EditObservation extends OpenHandsObservationEvent<"edit"> {
source: "agent";
extras: {
path: string;
diff: string;
};
}

View File

@@ -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.

View File

@@ -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:

View File

@@ -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 .',
}