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