Spaces:
Build error
Build error
import json | |
import re | |
import traceback | |
from dataclasses import dataclass, field | |
from typing import Any, Self | |
from pydantic import BaseModel | |
from openhands.core.logger import openhands_logger as logger | |
from openhands.core.schema import ObservationType | |
from openhands.events.observation.observation import Observation | |
CMD_OUTPUT_PS1_BEGIN = '\n###PS1JSON###\n' | |
CMD_OUTPUT_PS1_END = '\n###PS1END###' | |
CMD_OUTPUT_METADATA_PS1_REGEX = re.compile( | |
f'^{CMD_OUTPUT_PS1_BEGIN.strip()}(.*?){CMD_OUTPUT_PS1_END.strip()}', | |
re.DOTALL | re.MULTILINE, | |
) | |
class CmdOutputMetadata(BaseModel): | |
"""Additional metadata captured from PS1""" | |
exit_code: int = -1 | |
pid: int = -1 | |
username: str | None = None | |
hostname: str | None = None | |
working_dir: str | None = None | |
py_interpreter_path: str | None = None | |
prefix: str = '' # Prefix to add to command output | |
suffix: str = '' # Suffix to add to command output | |
def to_ps1_prompt(cls) -> str: | |
"""Convert the required metadata into a PS1 prompt.""" | |
prompt = CMD_OUTPUT_PS1_BEGIN | |
json_str = json.dumps( | |
{ | |
'pid': '$!', | |
'exit_code': '$?', | |
'username': r'\u', | |
'hostname': r'\h', | |
'working_dir': r'$(pwd)', | |
'py_interpreter_path': r'$(which python 2>/dev/null || echo "")', | |
}, | |
indent=2, | |
) | |
# Make sure we escape double quotes in the JSON string | |
# So that PS1 will keep them as part of the output | |
prompt += json_str.replace('"', r'\"') | |
prompt += CMD_OUTPUT_PS1_END + '\n' # Ensure there's a newline at the end | |
return prompt | |
def matches_ps1_metadata(cls, string: str) -> list[re.Match[str]]: | |
matches = [] | |
for match in CMD_OUTPUT_METADATA_PS1_REGEX.finditer(string): | |
try: | |
json.loads(match.group(1).strip()) # Try to parse as JSON | |
matches.append(match) | |
except json.JSONDecodeError: | |
logger.warning( | |
f'Failed to parse PS1 metadata: {match.group(1)}. Skipping.' | |
+ traceback.format_exc() | |
) | |
continue # Skip if not valid JSON | |
return matches | |
def from_ps1_match(cls, match: re.Match[str]) -> Self: | |
"""Extract the required metadata from a PS1 prompt.""" | |
metadata = json.loads(match.group(1)) | |
# Create a copy of metadata to avoid modifying the original | |
processed = metadata.copy() | |
# Convert numeric fields | |
if 'pid' in metadata: | |
try: | |
processed['pid'] = int(float(str(metadata['pid']))) | |
except (ValueError, TypeError): | |
processed['pid'] = -1 | |
if 'exit_code' in metadata: | |
try: | |
processed['exit_code'] = int(float(str(metadata['exit_code']))) | |
except (ValueError, TypeError): | |
logger.warning( | |
f'Failed to parse exit code: {metadata["exit_code"]}. Setting to -1.' | |
) | |
processed['exit_code'] = -1 | |
return cls(**processed) | |
class CmdOutputObservation(Observation): | |
"""This data class represents the output of a command.""" | |
command: str | |
observation: str = ObservationType.RUN | |
# Additional metadata captured from PS1 | |
metadata: CmdOutputMetadata = field(default_factory=CmdOutputMetadata) | |
# Whether the command output should be hidden from the user | |
hidden: bool = False | |
def __init__( | |
self, | |
content: str, | |
command: str, | |
observation: str = ObservationType.RUN, | |
metadata: dict[str, Any] | CmdOutputMetadata | None = None, | |
hidden: bool = False, | |
**kwargs: Any, | |
) -> None: | |
super().__init__(content) | |
self.command = command | |
self.observation = observation | |
self.hidden = hidden | |
if isinstance(metadata, dict): | |
self.metadata = CmdOutputMetadata(**metadata) | |
else: | |
self.metadata = metadata or CmdOutputMetadata() | |
# Handle legacy attribute | |
if 'exit_code' in kwargs: | |
self.metadata.exit_code = kwargs['exit_code'] | |
if 'command_id' in kwargs: | |
self.metadata.pid = kwargs['command_id'] | |
def command_id(self) -> int: | |
return self.metadata.pid | |
def exit_code(self) -> int: | |
return self.metadata.exit_code | |
def error(self) -> bool: | |
return self.exit_code != 0 | |
def message(self) -> str: | |
return f'Command `{self.command}` executed with exit code {self.exit_code}.' | |
def success(self) -> bool: | |
return not self.error | |
def __str__(self) -> str: | |
return ( | |
f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code}, ' | |
f'metadata={json.dumps(self.metadata.model_dump(), indent=2)})**\n' | |
'--BEGIN AGENT OBSERVATION--\n' | |
f'{self.to_agent_observation()}\n' | |
'--END AGENT OBSERVATION--' | |
) | |
def to_agent_observation(self) -> str: | |
ret = f'{self.metadata.prefix}{self.content}{self.metadata.suffix}' | |
if self.metadata.working_dir: | |
ret += f'\n[Current working directory: {self.metadata.working_dir}]' | |
if self.metadata.py_interpreter_path: | |
ret += f'\n[Python interpreter: {self.metadata.py_interpreter_path}]' | |
if self.metadata.exit_code != -1: | |
ret += f'\n[Command finished with exit code {self.metadata.exit_code}]' | |
return ret | |
class IPythonRunCellObservation(Observation): | |
"""This data class represents the output of a IPythonRunCellAction.""" | |
code: str | |
observation: str = ObservationType.RUN_IPYTHON | |
image_urls: list[str] | None = None | |
def error(self) -> bool: | |
return False # IPython cells do not return exit codes | |
def message(self) -> str: | |
return 'Code executed in IPython cell.' | |
def success(self) -> bool: | |
return True # IPython cells are always considered successful | |
def __str__(self) -> str: | |
result = f'**IPythonRunCellObservation**\n{self.content}' | |
if self.image_urls: | |
result += f'\nImages: {len(self.image_urls)}' | |
return result | |