Skip to content

Commit

Permalink
Handle error observations in codeact (#3383)
Browse files Browse the repository at this point in the history
* Handle error observations in codeact

* Remove comments
  • Loading branch information
neubig committed Aug 14, 2024
1 parent 19bc061 commit 7d331ac
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 5 deletions.
10 changes: 9 additions & 1 deletion agenthub/codeact_agent/codeact_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CmdOutputObservation,
IPythonRunCellObservation,
)
from opendevin.events.observation.error import ErrorObservation
from opendevin.events.observation.observation import Observation
from opendevin.events.serialization.event import truncate_content
from opendevin.llm.llm import LLM
Expand Down Expand Up @@ -169,7 +170,14 @@ def get_observation_message(self, obs: Observation) -> Message | None:
str(obs.outputs), max_message_chars
)
return Message(role='user', content=[TextContent(text=text)])
return None
elif isinstance(obs, ErrorObservation):
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
text += '\n[Error occurred in processing last action]'
return Message(role='user', content=[TextContent(text=text)])
else:
# If an observation message is not returned, it will cause an error
# when the LLM tries to return the next message
raise ValueError(f'Unknown observation type: {type(obs)}')

def reset(self) -> None:
"""Resets the CodeAct Agent."""
Expand Down
4 changes: 2 additions & 2 deletions opendevin/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,11 @@ async def on_event(self, event: Event):
elif isinstance(event, ModifyTaskAction):
self.state.root_task.set_subtask_state(event.task_id, event.state)
elif isinstance(event, AgentFinishAction):
self.state.outputs = event.outputs # type: ignore[attr-defined]
self.state.outputs = event.outputs
self.state.metrics.merge(self.state.local_metrics)
await self.set_agent_state_to(AgentState.FINISHED)
elif isinstance(event, AgentRejectAction):
self.state.outputs = event.outputs # type: ignore[attr-defined]
self.state.outputs = event.outputs
self.state.metrics.merge(self.state.local_metrics)
await self.set_agent_state_to(AgentState.REJECTED)
elif isinstance(event, Observation):
Expand Down
11 changes: 10 additions & 1 deletion opendevin/events/action/agent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from typing import Any

from opendevin.core.schema import ActionType

Expand Down Expand Up @@ -35,7 +36,15 @@ def __str__(self) -> str:

@dataclass
class AgentFinishAction(Action):
outputs: dict = field(default_factory=dict)
"""An action where the agent finishes the task.
Attributes:
outputs (dict): The outputs of the agent, for instance "content".
thought (str): The agent's explanation of its actions.
action (str): The action type, namely ActionType.FINISH.
"""

outputs: dict[str, Any] = field(default_factory=dict)
thought: str = ''
action: str = ActionType.FINISH

Expand Down
8 changes: 7 additions & 1 deletion opendevin/events/observation/delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

@dataclass
class AgentDelegateObservation(Observation):
"""This data class represents the result from delegating to another agent"""
"""This data class represents the result from delegating to another agent.
Attributes:
content (str): The content of the observation.
outputs (dict): The outputs of the delegated agent.
observation (str): The type of observation.
"""

outputs: dict
observation: str = ObservationType.DELEGATE
Expand Down
97 changes: 97 additions & 0 deletions tests/unit/test_codeact_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from unittest.mock import Mock

import pytest

from agenthub.codeact_agent.codeact_agent import CodeActAgent
from opendevin.core.config import LLMConfig
from opendevin.core.message import TextContent
from opendevin.events.observation.commands import (
CmdOutputObservation,
IPythonRunCellObservation,
)
from opendevin.events.observation.delegate import AgentDelegateObservation
from opendevin.events.observation.error import ErrorObservation
from opendevin.llm.llm import LLM


@pytest.fixture
def agent() -> CodeActAgent:
agent = CodeActAgent(llm=LLM(LLMConfig()))
agent.llm = Mock()
agent.llm.config = Mock()
agent.llm.config.max_message_chars = 100
return agent


def test_cmd_output_observation_message(agent: CodeActAgent):
obs = CmdOutputObservation(
command='echo hello', content='Command output', command_id=1, exit_code=0
)

result = agent.get_observation_message(obs)

assert result is not None
assert result.role == 'user'
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert 'OBSERVATION:' in result.content[0].text
assert 'Command output' in result.content[0].text
assert 'Command 1 finished with exit code 0' in result.content[0].text


def test_ipython_run_cell_observation_message(agent: CodeActAgent):
obs = IPythonRunCellObservation(
code='plt.plot()',
content='IPython output\n![image](data:image/png;base64,ABC123)',
)

result = agent.get_observation_message(obs)

assert result is not None
assert result.role == 'user'
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert 'OBSERVATION:' in result.content[0].text
assert 'IPython output' in result.content[0].text
assert (
'![image](data:image/png;base64, ...) already displayed to user'
in result.content[0].text
)
assert 'ABC123' not in result.content[0].text


def test_agent_delegate_observation_message(agent: CodeActAgent):
obs = AgentDelegateObservation(
content='Content', outputs={'content': 'Delegated agent output'}
)

result = agent.get_observation_message(obs)

assert result is not None
assert result.role == 'user'
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert 'OBSERVATION:' in result.content[0].text
assert 'Delegated agent output' in result.content[0].text


def test_error_observation_message(agent: CodeActAgent):
obs = ErrorObservation('Error message')

result = agent.get_observation_message(obs)

assert result is not None
assert result.role == 'user'
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert 'OBSERVATION:' in result.content[0].text
assert 'Error message' in result.content[0].text
assert 'Error occurred in processing last action' in result.content[0].text


def test_unknown_observation_message(agent: CodeActAgent):
obs = Mock()

with pytest.raises(ValueError) as excinfo:
agent.get_observation_message(obs)
assert 'Unknown observation type:' in str(excinfo.value)

0 comments on commit 7d331ac

Please sign in to comment.