Compare commits

...

5 Commits

7 changed files with 286 additions and 3 deletions

View File

@@ -1,13 +1,16 @@
import os
from typing import TYPE_CHECKING
from browsergym.core.action.highlevel import HighLevelActionSet
from browsergym.utils.obs import flatten_axtree_to_str
from openhands.agenthub.browsing_agent.response_parser import BrowsingResponseParser
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
# Import Agent for both type checking and runtime
from openhands.controller.agent import Agent
from openhands.core.message import Message, TextContent
from openhands.events.action import (
Action,
@@ -91,6 +94,8 @@ In order to accomplish my goal I need to click on the button with bid 12
return prompt
class BrowsingAgent(Agent):
VERSION = '1.0'
"""

View File

@@ -0,0 +1,6 @@
"""Core components of the OpenHands system."""
from openhands.core.conversation import Conversation
from openhands.core.openhands import OpenHands
__all__ = ['Conversation', 'OpenHands']

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.controller.agent_controller import AgentController
from openhands.events.stream import EventStream
from openhands.llm.llm import LLM
from openhands.runtime.base import Runtime
@dataclass
class Conversation:
"""Main interface for conversations in OpenHands.
This class serves as a container for all the components needed for a conversation
between a user and an OpenHands agent.
Attributes:
conversation_id: Unique identifier for the conversation
runtime: Runtime environment where the agent operates
llm: Language model used by the agent
event_stream: Stream of events (actions and observations) in the conversation
agent_controller: Controller that manages the agent's behavior and state
"""
conversation_id: str
runtime: 'Runtime'
llm: 'LLM'
event_stream: 'EventStream'
agent_controller: 'AgentController'

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.setup import create_agent, create_runtime
from openhands.events.stream import EventStream
from openhands.llm.llm import LLM
from openhands.storage import get_file_store
if TYPE_CHECKING:
from openhands.core.conversation import Conversation
class OpenHands:
"""Main class for creating and managing OpenHands conversations.
This class is responsible for creating new conversations based on the provided
configuration. It serves as the primary entry point for interacting with the
OpenHands system.
Attributes:
config: Configuration for the OpenHands system
"""
def __init__(self, config: OpenHandsConfig):
"""Initialize the OpenHands instance with the provided configuration.
Args:
config: Configuration for the OpenHands system
"""
self.config = config
def create_conversation(self, conversation_id: str | None = None) -> 'Conversation':
"""Create a new conversation with all necessary components.
This method creates a Runtime, LLM, EventStream, and AgentController according
to the configuration provided to the constructor, and returns a Conversation
object containing all these components.
Args:
conversation_id: Optional identifier for the conversation. If not provided,
a unique ID will be generated.
Returns:
A Conversation object containing all the components needed for interaction.
"""
# Create a runtime based on the configuration
runtime = create_runtime(self.config)
# Create a file store
file_store = get_file_store(self.config.file_store, self.config.file_store_path)
# Create an event stream for the conversation
# Generate a unique ID if none is provided
sid = (
conversation_id
if conversation_id is not None
else f'conversation-{uuid.uuid4()}'
)
event_stream = EventStream(sid=sid, file_store=file_store)
# Get the default LLM configuration and create an LLM instance
llm_config = self.config.get_llm_config()
llm = LLM(llm_config)
# Create an agent using the factory function
agent = create_agent(self.config)
# Create an agent controller
from openhands.controller.agent_controller import AgentController
agent_controller = AgentController(
agent=agent,
event_stream=event_stream,
max_iterations=self.config.max_iterations,
max_budget_per_task=self.config.max_budget_per_task,
agent_to_llm_config=self.config.get_agent_to_llm_config_map(),
agent_configs=self.config.get_agent_configs(),
sid=conversation_id,
)
# Create and return a Conversation object
from openhands.core.conversation import Conversation
return Conversation(
conversation_id=conversation_id or event_stream.sid,
runtime=runtime,
llm=llm,
event_stream=event_stream,
agent_controller=agent_controller,
)

View File

@@ -1,3 +1,4 @@
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, Protocol
@@ -6,9 +7,11 @@ from httpx import AsyncClient, HTTPError, HTTPStatusError
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel, SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import AppMode
# Use standard logging instead of importing from core.logger
logger = logging.getLogger('openhands')
class ProviderType(Enum):
GITHUB = 'github'

View File

@@ -1,4 +1,5 @@
import json
import logging
import os
import urllib.parse
from typing import Union
@@ -6,9 +7,11 @@ from typing import Union
import httpx
from requests.exceptions import RequestException
from openhands.core.logger import openhands_logger as logger
from openhands.storage.files import FileStore
# Use standard logging instead of importing from core.logger
logger = logging.getLogger('openhands')
class HTTPFileStore(FileStore):
"""

View File

@@ -0,0 +1,141 @@
import unittest
from unittest.mock import MagicMock, patch
from openhands.core import Conversation, OpenHands
from openhands.core.config.openhands_config import OpenHandsConfig
class TestConversation(unittest.TestCase):
def test_conversation_dataclass(self):
"""Test that the Conversation class is a dataclass with the expected fields."""
# Create mock objects for the constructor
conversation_id = 'test-conversation-id'
runtime = MagicMock()
llm = MagicMock()
event_stream = MagicMock()
agent_controller = MagicMock()
# Create a Conversation instance
conversation = Conversation(
conversation_id=conversation_id,
runtime=runtime,
llm=llm,
event_stream=event_stream,
agent_controller=agent_controller,
)
# Verify that the fields are set correctly
self.assertEqual(conversation.conversation_id, conversation_id)
self.assertEqual(conversation.runtime, runtime)
self.assertEqual(conversation.llm, llm)
self.assertEqual(conversation.event_stream, event_stream)
self.assertEqual(conversation.agent_controller, agent_controller)
class TestOpenHands(unittest.TestCase):
@patch('openhands.core.openhands.create_runtime')
@patch('openhands.core.openhands.EventStream')
@patch('openhands.core.openhands.LLM')
@patch('openhands.core.openhands.create_agent')
@patch('openhands.controller.agent_controller.AgentController')
def test_create_conversation(
self,
mock_agent_controller,
mock_create_agent,
mock_llm,
mock_event_stream,
mock_create_runtime,
):
"""Test that the create_conversation method creates all the expected components."""
# Create mock objects
config = OpenHandsConfig()
mock_runtime = MagicMock()
mock_create_runtime.return_value = mock_runtime
mock_event_stream_instance = MagicMock()
mock_event_stream_instance.sid = 'generated-id'
mock_event_stream.return_value = mock_event_stream_instance
mock_llm_instance = MagicMock()
mock_llm.return_value = mock_llm_instance
mock_agent_instance = MagicMock()
mock_create_agent.return_value = mock_agent_instance
mock_agent_controller_instance = MagicMock()
mock_agent_controller.return_value = mock_agent_controller_instance
# Create an OpenHands instance and call create_conversation
openhands = OpenHands(config)
conversation = openhands.create_conversation()
# Verify that all the expected methods were called
mock_create_runtime.assert_called_with(config)
mock_event_stream.assert_called_once()
mock_llm.assert_called_once()
mock_create_agent.assert_called_once()
mock_agent_controller.assert_called_once()
# Verify that the returned Conversation has the expected attributes
self.assertEqual(conversation.conversation_id, 'generated-id')
self.assertEqual(conversation.runtime, mock_runtime)
self.assertEqual(conversation.llm, mock_llm_instance)
self.assertEqual(conversation.event_stream, mock_event_stream_instance)
self.assertEqual(conversation.agent_controller, mock_agent_controller_instance)
@patch('openhands.core.openhands.create_runtime')
@patch('openhands.core.openhands.EventStream')
@patch('openhands.core.openhands.LLM')
@patch('openhands.core.openhands.create_agent')
@patch('openhands.controller.agent_controller.AgentController')
def test_create_conversation_with_id(
self,
mock_agent_controller,
mock_create_agent,
mock_llm,
mock_event_stream,
mock_create_runtime,
):
"""Test that the create_conversation method uses the provided conversation_id."""
# Create mock objects
config = OpenHandsConfig()
conversation_id = 'custom-conversation-id'
mock_runtime = MagicMock()
mock_create_runtime.return_value = mock_runtime
mock_event_stream_instance = MagicMock()
mock_event_stream.return_value = mock_event_stream_instance
mock_llm_instance = MagicMock()
mock_llm.return_value = mock_llm_instance
mock_agent_instance = MagicMock()
mock_create_agent.return_value = mock_agent_instance
mock_agent_controller_instance = MagicMock()
mock_agent_controller.return_value = mock_agent_controller_instance
# Create an OpenHands instance and call create_conversation with a custom ID
openhands = OpenHands(config)
conversation = openhands.create_conversation(conversation_id=conversation_id)
# Verify that the EventStream and AgentController were created with the custom ID
# The EventStream is called with both sid and file_store
self.assertEqual(mock_event_stream.call_args.kwargs['sid'], conversation_id)
mock_agent_controller.assert_called_once_with(
agent=mock_agent_instance,
event_stream=mock_event_stream_instance,
max_iterations=config.max_iterations,
max_budget_per_task=config.max_budget_per_task,
agent_to_llm_config=config.get_agent_to_llm_config_map(),
agent_configs=config.get_agent_configs(),
sid=conversation_id,
)
# Verify that the returned Conversation has the custom ID
self.assertEqual(conversation.conversation_id, conversation_id)
if __name__ == '__main__':
unittest.main()