mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Revamp Exception handling (#1080)
* Revamp exception handling * Agent controller: sleep 3 seconds if APIConnection error * Fix AuthenticationError capture * Revert unrelated style fixes * Add type enforcement for action_from_dict call
This commit is contained in:
parent
dd32fa6f4a
commit
53f95056de
@ -3,6 +3,7 @@ from opendevin.agent import Agent
|
||||
from opendevin.state import State
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.schema import ActionType, ObservationType
|
||||
from opendevin.exceptions import AgentNoInstructionError
|
||||
|
||||
from opendevin.action import (
|
||||
Action,
|
||||
@ -131,14 +132,14 @@ class MonologueAgent(Agent):
|
||||
- task (str): The initial goal statement provided by the user
|
||||
|
||||
Raises:
|
||||
- ValueError: If task is not provided
|
||||
- AgentNoInstructionError: If task is not provided
|
||||
"""
|
||||
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if task is None or task == '':
|
||||
raise ValueError('Instruction must be provided')
|
||||
raise AgentNoInstructionError()
|
||||
self.monologue = Monologue()
|
||||
self.memory = LongTermMemory()
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.exceptions import AgentEventTypeError
|
||||
import agenthub.monologue_agent.utils.json as json
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
|
||||
@ -24,10 +26,10 @@ class Monologue:
|
||||
- t (dict): The thought that we want to add to memory
|
||||
|
||||
Raises:
|
||||
- ValueError: If t is not a dict
|
||||
- AgentEventTypeError: If t is not a dict
|
||||
"""
|
||||
if not isinstance(t, dict):
|
||||
raise ValueError('Event must be a dictionary')
|
||||
raise AgentEventTypeError()
|
||||
self.thoughts.append(t)
|
||||
|
||||
def get_thoughts(self):
|
||||
|
||||
@ -12,6 +12,7 @@ from opendevin.action import (
|
||||
from opendevin.observation import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
from opendevin.exceptions import LLMOutputError
|
||||
|
||||
ACTION_PROMPT = """
|
||||
You're a thoughtful robot. Your main task is this:
|
||||
@ -170,7 +171,7 @@ def parse_action_response(response: str) -> Action:
|
||||
try:
|
||||
action_dict = json.loads(max(response_json_matches, key=rank)[0]) # Use the highest ranked response
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
raise LLMOutputError(
|
||||
"Output from the LLM isn't properly formatted. The model may be misconfigured."
|
||||
) from e
|
||||
if 'content' in action_dict:
|
||||
|
||||
@ -28,6 +28,8 @@ ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in ac
|
||||
|
||||
|
||||
def action_from_dict(action: dict) -> Action:
|
||||
if not isinstance(action, dict):
|
||||
raise TypeError('action must be a dictionary')
|
||||
action = action.copy()
|
||||
if 'action' not in action:
|
||||
raise KeyError(f"'action' key is not found in {action=}")
|
||||
|
||||
@ -5,6 +5,7 @@ if TYPE_CHECKING:
|
||||
from opendevin.action import Action
|
||||
from opendevin.state import State
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.exceptions import AgentAlreadyRegisteredError, AgentNotRegisteredError
|
||||
|
||||
|
||||
class Agent(ABC):
|
||||
@ -71,9 +72,12 @@ class Agent(ABC):
|
||||
Parameters:
|
||||
- name (str): The name to register the class under.
|
||||
- agent_cls (Type['Agent']): The class to register.
|
||||
|
||||
Raises:
|
||||
- AgentAlreadyRegisteredError: If name already registered
|
||||
"""
|
||||
if name in cls._registry:
|
||||
raise ValueError(f"Agent class already registered under '{name}'.")
|
||||
raise AgentAlreadyRegisteredError(name)
|
||||
cls._registry[name] = agent_cls
|
||||
|
||||
@classmethod
|
||||
@ -86,16 +90,22 @@ class Agent(ABC):
|
||||
|
||||
Returns:
|
||||
- agent_cls (Type['Agent']): The class registered under the specified name.
|
||||
|
||||
Raises:
|
||||
- AgentNotRegisteredError: If name not registered
|
||||
"""
|
||||
if name not in cls._registry:
|
||||
raise ValueError(f"No agent class registered under '{name}'.")
|
||||
raise AgentNotRegisteredError(name)
|
||||
return cls._registry[name]
|
||||
|
||||
@classmethod
|
||||
def list_agents(cls) -> list[str]:
|
||||
"""
|
||||
Retrieves the list of all agent names from the registry.
|
||||
|
||||
Raises:
|
||||
- AgentNotRegisteredError: If no agent is registered
|
||||
"""
|
||||
if not bool(cls._registry):
|
||||
raise ValueError('No agent class registered.')
|
||||
raise AgentNotRegisteredError()
|
||||
return list(cls._registry.keys())
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import traceback
|
||||
import time
|
||||
from typing import List, Callable, Literal, Mapping, Awaitable, Any, cast
|
||||
|
||||
from termcolor import colored
|
||||
from litellm.exceptions import APIConnectionError
|
||||
from openai import AuthenticationError
|
||||
|
||||
from opendevin import config
|
||||
from opendevin.action import (
|
||||
@ -15,7 +18,7 @@ from opendevin.action import (
|
||||
)
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.exceptions import MaxCharsExceedError
|
||||
from opendevin.exceptions import MaxCharsExceedError, AgentNoActionError
|
||||
from opendevin.observation import Observation, AgentErrorObservation, NullObservation
|
||||
from opendevin.plan import Plan
|
||||
from opendevin.state import State
|
||||
@ -102,9 +105,11 @@ class AgentController:
|
||||
|
||||
def add_history(self, action: Action, observation: Observation):
|
||||
if not isinstance(action, Action):
|
||||
raise ValueError('action must be an instance of Action')
|
||||
raise TypeError(
|
||||
f'action must be an instance of Action, got {type(action).__name__} instead')
|
||||
if not isinstance(observation, Observation):
|
||||
raise ValueError('observation must be an instance of Observation')
|
||||
raise TypeError(
|
||||
f'observation must be an instance of Observation, got {type(observation).__name__} instead')
|
||||
self.state.history.append((action, observation))
|
||||
self.state.updated_info.append((action, observation))
|
||||
|
||||
@ -144,17 +149,23 @@ class AgentController:
|
||||
try:
|
||||
action = self.agent.step(self.state)
|
||||
if action is None:
|
||||
raise ValueError('Agent must return an action')
|
||||
raise AgentNoActionError()
|
||||
print_with_color(action, 'ACTION')
|
||||
except Exception as e:
|
||||
observation = AgentErrorObservation(str(e))
|
||||
print_with_color(observation, 'ERROR')
|
||||
traceback.print_exc()
|
||||
# TODO Change to more robust error handling
|
||||
if (
|
||||
'The api_key client option must be set' in observation.content
|
||||
or 'Incorrect API key provided:' in observation.content
|
||||
):
|
||||
if isinstance(e, APIConnectionError):
|
||||
time.sleep(3)
|
||||
|
||||
# raise specific exceptions that need to be handled outside
|
||||
# note: we are using AuthenticationError class from openai rather than
|
||||
# litellm because:
|
||||
# 1) litellm.exceptions.AuthenticationError is a subclass of openai.AuthenticationError
|
||||
# 2) embeddings call, initiated by llama-index, has no wrapper for authentication
|
||||
# errors. This means we have to catch individual authentication errors
|
||||
# from different providers, and OpenAI is one of these.
|
||||
if isinstance(e, (AuthenticationError, AgentNoActionError)):
|
||||
raise
|
||||
self.update_state_after_step()
|
||||
|
||||
|
||||
@ -5,3 +5,59 @@ class MaxCharsExceedError(Exception):
|
||||
else:
|
||||
message = 'Number of characters exceeds MAX_CHARS limit'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AgentNoActionError(Exception):
|
||||
def __init__(self, message='Agent must return an action'):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AgentNoInstructionError(Exception):
|
||||
def __init__(self, message='Instruction must be provided'):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AgentEventTypeError(Exception):
|
||||
def __init__(self, message='Event must be a dictionary'):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AgentAlreadyRegisteredError(Exception):
|
||||
def __init__(self, name=None):
|
||||
if name is not None:
|
||||
message = f"Agent class already registered under '{name}'"
|
||||
else:
|
||||
message = 'Agent class already registered'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AgentNotRegisteredError(Exception):
|
||||
def __init__(self, name=None):
|
||||
if name is not None:
|
||||
message = f"No agent class registered under '{name}'"
|
||||
else:
|
||||
message = 'No agent class registered'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class LLMOutputError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class SandboxInvalidBackgroundCommandError(Exception):
|
||||
def __init__(self, id=None):
|
||||
if id is not None:
|
||||
message = f'Invalid background command id {id}'
|
||||
else:
|
||||
message = 'Invalid background command id'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class PlanInvalidStateError(Exception):
|
||||
def __init__(self, state=None):
|
||||
if state is not None:
|
||||
message = f'Invalid state {state}'
|
||||
else:
|
||||
message = 'Invalid state'
|
||||
super().__init__(message)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.exceptions import PlanInvalidStateError
|
||||
|
||||
OPEN_STATE = 'open'
|
||||
COMPLETED_STATE = 'completed'
|
||||
@ -87,11 +89,11 @@ class Task:
|
||||
Args: state: The new state of the task.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided state is invalid.
|
||||
PlanInvalidStateError: If the provided state is invalid.
|
||||
"""
|
||||
if state not in STATES:
|
||||
logger.error('Invalid state: %s', state)
|
||||
raise ValueError('Invalid state:' + state)
|
||||
raise PlanInvalidStateError(state)
|
||||
self.state = state
|
||||
if state == COMPLETED_STATE or state == ABANDONED_STATE or state == VERIFIED_STATE:
|
||||
for subtask in self.subtasks:
|
||||
|
||||
@ -13,6 +13,7 @@ from opendevin import config
|
||||
from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.sandbox import Sandbox, BackgroundCommand
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.exceptions import SandboxInvalidBackgroundCommandError
|
||||
|
||||
InputType = namedtuple('InputType', ['content'])
|
||||
OutputType = namedtuple('OutputType', ['content'])
|
||||
@ -107,7 +108,7 @@ class DockerExecBox(Sandbox):
|
||||
|
||||
def read_logs(self, id) -> str:
|
||||
if id not in self.background_commands:
|
||||
raise ValueError('Invalid background command id')
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
return bg_cmd.read_logs()
|
||||
|
||||
@ -157,7 +158,7 @@ class DockerExecBox(Sandbox):
|
||||
|
||||
def kill_background(self, id: int) -> BackgroundCommand:
|
||||
if id not in self.background_commands:
|
||||
raise ValueError('Invalid background command id')
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
if bg_cmd.pid is not None:
|
||||
self.container.exec_run(
|
||||
|
||||
@ -15,6 +15,7 @@ from opendevin.logger import opendevin_logger as logger
|
||||
from opendevin.sandbox.sandbox import Sandbox, BackgroundCommand
|
||||
from opendevin.schema import ConfigType
|
||||
from opendevin.utils import find_available_tcp_port
|
||||
from opendevin.exceptions import SandboxInvalidBackgroundCommandError
|
||||
|
||||
InputType = namedtuple('InputType', ['content'])
|
||||
OutputType = namedtuple('OutputType', ['content'])
|
||||
@ -187,7 +188,7 @@ class DockerSSHBox(Sandbox):
|
||||
|
||||
def read_logs(self, id) -> str:
|
||||
if id not in self.background_commands:
|
||||
raise ValueError('Invalid background command id')
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
return bg_cmd.read_logs()
|
||||
|
||||
@ -238,7 +239,7 @@ class DockerSSHBox(Sandbox):
|
||||
|
||||
def kill_background(self, id: int) -> BackgroundCommand:
|
||||
if id not in self.background_commands:
|
||||
raise ValueError('Invalid background command id')
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
if bg_cmd.pid is not None:
|
||||
self.container.exec_run(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user