Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/01_standalone_sdk/02_custom_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Action,
Agent,
Conversation,
ConversationState,
Event,
ImageContent,
LLMConvertibleEvent,
Expand Down Expand Up @@ -124,7 +125,10 @@ class GrepTool(ToolDefinition[GrepAction, GrepObservation]):

@classmethod
def create(
cls, conv_state, bash_executor: BashExecutor | None = None
cls,
conv_state: ConversationState,
bash_executor: BashExecutor | None = None,
**kwargs, # noqa: ARG003
) -> Sequence[ToolDefinition]:
"""Create GrepTool instance with a GrepExecutor.

Expand Down
2 changes: 2 additions & 0 deletions openhands-sdk/openhands/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
BaseConversation,
Conversation,
ConversationCallbackType,
ConversationState,
LocalConversation,
RemoteConversation,
)
Expand Down Expand Up @@ -81,6 +82,7 @@
"LocalConversation",
"RemoteConversation",
"ConversationCallbackType",
"ConversationState",
"Event",
"LLMConvertibleEvent",
"AgentContext",
Expand Down
2 changes: 1 addition & 1 deletion openhands-sdk/openhands/sdk/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _initialize(self, state: "ConversationState"):

# Add MCP tools if configured
if self.mcp_config:
mcp_tools = create_mcp_tools(self.mcp_config, timeout=30)
mcp_tools = create_mcp_tools(state, self.mcp_config, timeout=30)
tools.extend(mcp_tools)

logger.info(
Expand Down
5 changes: 4 additions & 1 deletion openhands-sdk/openhands/sdk/mcp/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


if TYPE_CHECKING:
from openhands.sdk.conversation import LocalConversation
from openhands.sdk.conversation import ConversationState, LocalConversation

import mcp.types
from litellm import ChatCompletionToolParam
Expand Down Expand Up @@ -188,8 +188,11 @@ def action_from_arguments(self, arguments: dict[str, Any]) -> MCPToolAction:
@classmethod
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
*,
mcp_tool: mcp.types.Tool,
mcp_client: MCPClient,
**kwargs, # noqa: ARG003
) -> Sequence["MCPToolDefinition"]:
try:
annotations = (
Expand Down
16 changes: 13 additions & 3 deletions openhands-sdk/openhands/sdk/mcp/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions for MCP integration."""

import logging
from typing import TYPE_CHECKING

import mcp.types
from fastmcp.client.logging import LogMessage
Expand All @@ -11,6 +12,10 @@
from openhands.sdk.tool.tool import ToolDefinition


if TYPE_CHECKING:
from openhands.sdk.conversation import ConversationState


logger = get_logger(__name__)
LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()

Expand All @@ -30,7 +35,9 @@ async def log_handler(message: LogMessage):
logger.log(level, msg, extra=extra)


async def _list_tools(client: MCPClient) -> list[ToolDefinition]:
async def _list_tools(
conv_state: "ConversationState", client: MCPClient
) -> list[ToolDefinition]:
"""List tools from an MCP client."""
tools: list[ToolDefinition] = []

Expand All @@ -39,7 +46,7 @@ async def _list_tools(client: MCPClient) -> list[ToolDefinition]:
mcp_type_tools: list[mcp.types.Tool] = await client.list_tools()
for mcp_tool in mcp_type_tools:
tool_sequence = MCPToolDefinition.create(
mcp_tool=mcp_tool, mcp_client=client
conv_state=conv_state, mcp_tool=mcp_tool, mcp_client=client
)
tools.extend(tool_sequence) # Flatten sequence into list
assert not client.is_connected(), (
Expand All @@ -49,6 +56,7 @@ async def _list_tools(client: MCPClient) -> list[ToolDefinition]:


def create_mcp_tools(
conv_state: "ConversationState",
config: dict | MCPConfig,
timeout: float = 30.0,
) -> list[MCPToolDefinition]:
Expand All @@ -57,7 +65,9 @@ def create_mcp_tools(
if isinstance(config, dict):
config = MCPConfig.model_validate(config)
client = MCPClient(config, log_handler=log_handler)
tools = client.call_async_from_sync(_list_tools, timeout=timeout, client=client)
tools = client.call_async_from_sync(
_list_tools, timeout=timeout, conv_state=conv_state, client=client
)

logger.info(f"Created {len(tools)} MCP tools: {[t.name for t in tools]}")
return tools
4 changes: 2 additions & 2 deletions openhands-sdk/openhands/sdk/tool/builtins/finish.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ class FinishTool(ToolDefinition[FinishAction, FinishObservation]):
@classmethod
def create(
cls,
conv_state: "ConversationState | None" = None, # noqa: ARG003
conv_state: "ConversationState", # noqa: ARG003
**params,
) -> Sequence[Self]:
"""Create FinishTool instance.

Args:
conv_state: Optional conversation state (not used by FinishTool).
conv_state: Conversation state (not used by FinishTool).
**params: Additional parameters (none supported).

Returns:
Expand Down
4 changes: 2 additions & 2 deletions openhands-sdk/openhands/sdk/tool/builtins/think.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ class ThinkTool(ToolDefinition[ThinkAction, ThinkObservation]):
@classmethod
def create(
cls,
conv_state: "ConversationState | None" = None, # noqa: ARG003
conv_state: "ConversationState", # noqa: ARG003
**params,
) -> Sequence[Self]:
"""Create ThinkTool instance.

Args:
conv_state: Optional conversation state (not used by ThinkTool).
conv_state: Conversation state (not used by ThinkTool).
**params: Additional parameters (none supported).

Returns:
Expand Down
6 changes: 3 additions & 3 deletions openhands-sdk/openhands/sdk/tool/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@


if TYPE_CHECKING:
from openhands.sdk.conversation import LocalConversation
from openhands.sdk.conversation import ConversationState, LocalConversation


ActionT = TypeVar("ActionT", bound=Action)
Expand Down Expand Up @@ -179,15 +179,15 @@ def create(cls, conv_state, **params):

@classmethod
@abstractmethod
def create(cls, *args, **kwargs) -> Sequence[Self]:
def create(cls, conv_state: "ConversationState", **kwargs) -> Sequence[Self]:
"""Create a sequence of Tool instances.

This method must be implemented by all subclasses to provide custom
initialization logic, typically initializing the executor with parameters
from conv_state and other optional parameters.

Args:
*args: Variable positional arguments (typically conv_state as first arg).
conv_state: Conversation state containing workspace and other context.
**kwargs: Optional parameters for tool initialization.

Returns:
Expand Down
97 changes: 85 additions & 12 deletions openhands-tools/openhands/tools/browser_use/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

# Lazy import to avoid hanging during module import
if TYPE_CHECKING:
from openhands.tools.browser_use.impl import BrowserToolExecutor
from openhands.sdk.conversation.state import ConversationState


# Maximum output size for browser observations
Expand Down Expand Up @@ -102,7 +102,14 @@ class BrowserNavigateTool(ToolDefinition[BrowserNavigateAction, BrowserObservati
"""Tool for browser navigation."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_navigate",
Expand Down Expand Up @@ -153,7 +160,14 @@ class BrowserClickTool(ToolDefinition[BrowserClickAction, BrowserObservation]):
"""Tool for clicking browser elements."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_click",
Expand Down Expand Up @@ -201,7 +215,14 @@ class BrowserTypeTool(ToolDefinition[BrowserTypeAction, BrowserObservation]):
"""Tool for typing text into browser elements."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_type",
Expand Down Expand Up @@ -246,7 +267,14 @@ class BrowserGetStateTool(ToolDefinition[BrowserGetStateAction, BrowserObservati
"""Tool for getting browser state."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_get_state",
Expand Down Expand Up @@ -294,7 +322,14 @@ class BrowserGetContentTool(
"""Tool for getting page content in markdown."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_get_content",
Expand Down Expand Up @@ -339,7 +374,14 @@ class BrowserScrollTool(ToolDefinition[BrowserScrollAction, BrowserObservation])
"""Tool for scrolling the browser page."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_scroll",
Expand Down Expand Up @@ -378,7 +420,14 @@ class BrowserGoBackTool(ToolDefinition[BrowserGoBackAction, BrowserObservation])
"""Tool for going back in browser history."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_go_back",
Expand Down Expand Up @@ -417,7 +466,14 @@ class BrowserListTabsTool(ToolDefinition[BrowserListTabsAction, BrowserObservati
"""Tool for listing browser tabs."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_list_tabs",
Expand Down Expand Up @@ -461,7 +517,14 @@ class BrowserSwitchTabTool(ToolDefinition[BrowserSwitchTabAction, BrowserObserva
"""Tool for switching browser tabs."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_switch_tab",
Expand Down Expand Up @@ -504,7 +567,14 @@ class BrowserCloseTabTool(ToolDefinition[BrowserCloseTabAction, BrowserObservati
"""Tool for closing browser tabs."""

@classmethod
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
def create(
cls,
conv_state: "ConversationState", # noqa: ARG003
**kwargs,
) -> Sequence[Self]:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = kwargs.get("executor") or BrowserToolExecutor(**kwargs)
return [
cls(
name="browser_close_tab",
Expand Down Expand Up @@ -536,13 +606,16 @@ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
@classmethod
def create(
cls,
conv_state: "ConversationState",
**executor_config,
) -> list[ToolDefinition[BrowserAction, BrowserObservation]]:
# Import executor only when actually needed to
# avoid hanging during module import
from openhands.tools.browser_use.impl import BrowserToolExecutor

# Create a single shared executor for all tools
executor = BrowserToolExecutor(**executor_config)

# Each tool.create() returns a Sequence[Self], so we flatten the results
tools: list[ToolDefinition[BrowserAction, BrowserObservation]] = []
for tool_class in [
Expand All @@ -557,5 +630,5 @@ def create(
BrowserSwitchTabTool,
BrowserCloseTabTool,
]:
tools.extend(tool_class.create(executor))
tools.extend(tool_class.create(conv_state, executor=executor))
return tools
1 change: 1 addition & 0 deletions openhands-tools/openhands/tools/delegate/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def create(
cls,
conv_state: "ConversationState",
max_children: int = 5,
**kwargs, # noqa: ARG003
) -> Sequence["DelegateTool"]:
"""Initialize DelegateTool with a DelegateExecutor.

Expand Down
1 change: 1 addition & 0 deletions openhands-tools/openhands/tools/execute_bash/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def create(
no_change_timeout_seconds: int | None = None,
terminal_type: Literal["tmux", "subprocess"] | None = None,
executor: ToolExecutor | None = None,
**kwargs, # noqa: ARG003
) -> Sequence["BashTool"]:
"""Initialize BashTool with executor parameters.

Expand Down
1 change: 1 addition & 0 deletions openhands-tools/openhands/tools/file_editor/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class FileEditorTool(ToolDefinition[FileEditorAction, FileEditorObservation]):
def create(
cls,
conv_state: "ConversationState",
**kwargs, # noqa: ARG003
) -> Sequence["FileEditorTool"]:
"""Initialize FileEditorTool with a FileEditorExecutor.

Expand Down
Loading
Loading