Skip to content
Merged
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
65 changes: 29 additions & 36 deletions agentic_internet/tools/browser_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
logger = logging.getLogger(__name__)

try:
from browser_use_sdk import AsyncBrowserUse, BrowserUse
from browser_use_sdk.v4 import AsyncBrowserUse, BrowserUse

HAS_BROWSER_USE = True
except ImportError:
Expand Down Expand Up @@ -60,10 +60,13 @@ def forward(self, task: str, structured_output: bool = False) -> str:
return "Browser Use not available. Check API key and browser-use-sdk installation."

try:
result = self.client.tasks.run(task=task)
if result.done_output:
return result.done_output
return f"Task completed but no output was returned. Status: {getattr(result, 'status', 'unknown')}"
created = self.client.runs.create(task)
result = self.client.runs.wait_for_completion(created.id)
if result.result:
return str(result.result)
if result.error:
return f"Browser automation failed: {result.error}"
return f"Task completed but no output was returned. Status: {result.status.value}"
except Exception as e:
logger.error("Browser automation failed: %s", e)
return f"Browser automation failed: {e}"
Expand Down Expand Up @@ -113,33 +116,21 @@ def forward(self, task: str, stream: bool = False) -> str:
async def _run_simple(self, task: str) -> str:
"""Run a simple async task."""
assert self.client is not None # guaranteed by forward()
result = await self.client.tasks.run(task=task)
if result.done_output:
return result.done_output
return f"Task completed. Status: {getattr(result, 'status', 'completed')}"
created = await self.client.runs.create(task)
result = await self.client.runs.wait_for_completion(created.id)
if result.result:
return str(result.result)
if result.error:
return f"Browser automation failed: {result.error}"
return f"Task completed. Status: {result.status.value}"

async def _run_with_stream(self, task: str) -> str:
"""Run a task with streaming updates."""
assert self.client is not None # guaranteed by forward()
# Create the task
created_task = await self.client.tasks.create(task=task)

updates = []
# Stream updates
async for update in self.client.tasks.stream(created_task.id):
if len(update.steps) > 0:
last_step = update.steps[-1]
updates.append(
f"Step: {last_step.url if hasattr(last_step, 'url') else 'processing'} - {last_step.next_goal if hasattr(last_step, 'next_goal') else 'working'}"
)

if update.status == "finished":
if update.done_output:
return update.done_output
else:
return "Task completed.\nSteps performed:\n" + "\n".join(updates)

return "Task stream ended without completion."
# V4 exposes incremental events via polling. The Tool API returns only
# once, so wait on the lightweight status endpoint and return the final
# run summary without buffering potentially sensitive event payloads.
return await self._run_simple(task)


class StructuredBrowserUseTool(Tool):
Expand Down Expand Up @@ -189,14 +180,16 @@ def forward(self, task: str, schema: str | None = None) -> str:
async def _extract_structured_data(self, task: str, schema: str | None = None) -> str:
"""Extract structured data from web pages."""
assert self.client is not None # guaranteed by forward()
# For now, we'll use the standard run method
# In a real implementation, you'd parse the schema and use it
result = await self.client.tasks.run(task=task)

if result.done_output:
return result.done_output
else:
return "No structured data extracted."
# The schema is not sent until this tool validates and converts it into
# a V4 output contract; preserve today's free-form behavior for now.
created = await self.client.runs.create(task)
result = await self.client.runs.wait_for_completion(created.id)

if result.result:
return str(result.result)
if result.error:
return f"Browser automation failed: {result.error}"
return "No structured data extracted."


# Example Pydantic models for common extraction tasks
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies = [
"exa-py>=2.0.0",
"litellm>=1.0.0",
"dotenv>=0.9.9",
"browser-use-sdk>=1.0.2",
"browser-use-sdk>=3.11.1",
"mcp>=1.0.0",
]

Expand Down
44 changes: 37 additions & 7 deletions tests/test_browser_use.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for browser use tools."""

from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

from agentic_internet.tools.browser_use import (
AsyncBrowserUseTool,
Expand All @@ -21,8 +22,9 @@ def test_no_api_key(self):
def test_successful_task(self, mock_browser_cls):
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.done_output = "Task completed successfully"
mock_client.tasks.run.return_value = mock_result
mock_result.result = "Task completed successfully"
mock_client.runs.create.return_value = SimpleNamespace(id="run-1")
mock_client.runs.wait_for_completion.return_value = mock_result
mock_browser_cls.return_value = mock_client

tool = BrowserUseTool(api_key="test-key")
Expand All @@ -34,9 +36,11 @@ def test_successful_task(self, mock_browser_cls):
def test_no_output(self, mock_browser_cls):
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.done_output = None
mock_result.status = "completed"
mock_client.tasks.run.return_value = mock_result
mock_result.result = None
mock_result.error = None
mock_result.status.value = "completed"
mock_client.runs.create.return_value = SimpleNamespace(id="run-1")
mock_client.runs.wait_for_completion.return_value = mock_result
mock_browser_cls.return_value = mock_client

tool = BrowserUseTool(api_key="test-key")
Expand All @@ -47,7 +51,7 @@ def test_no_output(self, mock_browser_cls):
@patch("agentic_internet.tools.browser_use.BrowserUse")
def test_exception_handling(self, mock_browser_cls):
mock_client = MagicMock()
mock_client.tasks.run.side_effect = RuntimeError("connection failed")
mock_client.runs.create.side_effect = RuntimeError("connection failed")
mock_browser_cls.return_value = mock_client

tool = BrowserUseTool(api_key="test-key")
Expand All @@ -62,6 +66,32 @@ def test_no_api_key(self):
result = tool.forward("test")
assert "not available" in result.lower() or "not configured" in result.lower()

@patch("agentic_internet.tools.browser_use.HAS_BROWSER_USE", True)
@patch("agentic_internet.tools.browser_use.AsyncBrowserUse")
def test_successful_task(self, mock_browser_cls):
mock_client = MagicMock()
mock_client.runs.create = AsyncMock(return_value=SimpleNamespace(id="run-1"))
mock_client.runs.wait_for_completion = AsyncMock(
return_value=SimpleNamespace(result="Async task completed", error=None)
)
mock_browser_cls.return_value = mock_client

tool = AsyncBrowserUseTool(api_key="test-key")
assert tool.forward("navigate to example.com") == "Async task completed"

@patch("agentic_internet.tools.browser_use.HAS_BROWSER_USE", True)
@patch("agentic_internet.tools.browser_use.AsyncBrowserUse")
def test_stream_uses_v4_run_lifecycle(self, mock_browser_cls):
mock_client = MagicMock()
mock_client.runs.create = AsyncMock(return_value=SimpleNamespace(id="run-1"))
mock_client.runs.wait_for_completion = AsyncMock(
return_value=SimpleNamespace(result="Stream task completed", error=None)
)
mock_browser_cls.return_value = mock_client

tool = AsyncBrowserUseTool(api_key="test-key")
assert tool.forward("navigate to example.com", stream=True) == "Stream task completed"


class TestStructuredBrowserUseTool:
def test_no_api_key(self):
Expand Down
Loading