Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2b68cd
Add Windows browser path checks to Chromium detection
SmartManoj Nov 4, 2025
7adba85
Merge branch 'main' into chromium
SmartManoj Nov 6, 2025
87938d3
format
SmartManoj Nov 6, 2025
87718f5
Refactor Windows Chromium path checks in impl.py
SmartManoj Nov 6, 2025
2dd4ab7
Merge branch 'main' into chromium
SmartManoj Nov 16, 2025
3614642
Merge branch 'main' into chromium
simonrosenberg Nov 19, 2025
2e0060a
Merge branch 'main' into chromium
SmartManoj Nov 20, 2025
aa05bf0
Refactor Windows browser path detection logic
SmartManoj Nov 20, 2025
3830639
Remove trailing whitespace in impl.py
SmartManoj Nov 20, 2025
0fa0e43
fix: allow detection of per-user Edge installations in LOCALAPPDATA
SmartManoj Nov 20, 2025
cb7086e
Rename browsers list to windows_browsers
SmartManoj Nov 21, 2025
c6c1ef8
Merge branch 'main' into chromium
SmartManoj Nov 21, 2025
b58f093
Merge branch 'main' into chromium
SmartManoj Dec 1, 2025
b5a56cd
Add Windows-specific browser executor implementation
SmartManoj Dec 3, 2025
3972616
lint
SmartManoj Dec 3, 2025
feb388b
Merge branch 'main' into chromium
SmartManoj Dec 3, 2025
881b20c
Apply suggestions from code review
SmartManoj Dec 3, 2025
c041799
Apply suggestions from code review
SmartManoj Dec 3, 2025
86054a9
Refactor Chromium detection logic for browser tool
SmartManoj Dec 3, 2025
544d515
Refactor Chromium detection tests to use executor classes
SmartManoj Dec 3, 2025
40d824d
Refactor tests to patch _ensure_chromium_available via class
SmartManoj Dec 3, 2025
b9d8b5b
Improve Windows Chrome detection logic
SmartManoj Dec 3, 2025
e952337
add new example to run on windows only
ryanhoangt Dec 3, 2025
93ac594
skip wait on fork PR
ryanhoangt Dec 3, 2025
fc2c451
exclude 30_windows.py example from doc check
ryanhoangt Dec 3, 2025
3b95b45
set visualizer to none
ryanhoangt Dec 3, 2025
54d93bc
update model name & base url
ryanhoangt Dec 3, 2025
1e3173e
Add standard browser installation path detection
SmartManoj Dec 3, 2025
352a97b
Rename 30_windows.py to 31_windows.py
SmartManoj Dec 3, 2025
dc29b77
Report LLM cost in Tom agent example
SmartManoj Dec 4, 2025
bc2c12e
Merge branch 'main' into chromium
SmartManoj Dec 4, 2025
696241f
Update Windows example filename references
SmartManoj Dec 4, 2025
d8a11d7
fix encoding error for windows example
ryanhoangt Dec 4, 2025
6ba981c
fix hidden dir
ryanhoangt Dec 4, 2025
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
12 changes: 12 additions & 0 deletions .github/scripts/check_documented_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
from pathlib import Path


EXEMPT_EXAMPLES = {
"examples/01_standalone_sdk/31_windows.py",
}


def find_documented_examples(docs_path: Path) -> set[str]:
"""
Find all example file references in the docs repository.
Expand Down Expand Up @@ -149,6 +154,13 @@ def main() -> None:
agent_examples = find_agent_sdk_examples(agent_sdk_root)
print(f" Found {len(agent_examples)} example file(s)")

exempt_present = agent_examples & EXEMPT_EXAMPLES
if exempt_present:
print(" Exempting the following example(s) from documentation check:")
for example in sorted(exempt_present):
print(f" - {example}")
agent_examples -= exempt_present

# Find all documented examples in docs
print("\n📄 Scanning docs repository...")
documented_examples = find_documented_examples(docs_path)
Expand Down
82 changes: 78 additions & 4 deletions .github/workflows/run-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ permissions:

jobs:
test-examples:
if: github.event.label.name == 'test-examples' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
needs: test-examples-windows
if: ${{ always() && (github.event.label.name == 'test-examples' || contains(fromJson('["workflow_dispatch","schedule"]'), github.event_name))
}}
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 60
steps:
- name: Wait for agent server to finish build
if: github.event_name == 'pull_request'
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
uses: lewagon/[email protected]
with:
ref: ${{ github.event.pull_request.head.ref }}
ref: ${{ github.event.pull_request.head.sha }}
check-name: Build & Push (python-amd64)
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
Expand All @@ -49,6 +53,13 @@ jobs:
with:
node-version: '22'

- name: Download Windows example results
if: needs.test-examples-windows.result == 'success'
uses: actions/download-artifact@v4
with:
name: examples-results-windows
path: .windows-example-results

- name: Install dependencies
run: uv sync --frozen --group dev

Expand Down Expand Up @@ -129,7 +140,13 @@ jobs:
uv run pytest tests/examples/test_examples.py \
--run-examples \
--examples-results-dir "$RESULTS_DIR" \
-n 4 || EXIT_CODE=$?
-n 4 \
-k "not 31_windows.py" \
|| EXIT_CODE=$?

if [ -d ".windows-example-results" ]; then
cp .windows-example-results/*.json "$RESULTS_DIR"/ || true
fi

TIMESTAMP="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
WORKFLOW_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
Expand Down Expand Up @@ -178,3 +195,60 @@ jobs:
**Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

${{ steps.read_report.outputs.report }}

test-examples-windows:
if: github.event.label.name == 'test-examples' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.ref || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install dependencies
run: uv sync --frozen --group dev

- name: Run Windows example
shell: bash
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_MODEL: litellm_proxy/claude-haiku-4-5-20251001
LLM_BASE_URL: https://llm-proxy.app.all-hands.dev
RUNTIME_API_KEY: ${{ secrets.RUNTIME_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
run: |
RESULTS_DIR=".example-test-results"
rm -rf "$RESULTS_DIR"
mkdir -p "$RESULTS_DIR"

uv run pytest tests/examples/test_examples.py \
--run-examples \
--examples-results-dir "$RESULTS_DIR" \
-n auto \
-k "31_windows.py"

- name: Upload Windows example results
if: always()
uses: actions/upload-artifact@v4
with:
name: examples-results-windows
path: .example-test-results
include-hidden-files: true
if-no-files-found: ignore

4 changes: 4 additions & 0 deletions examples/01_standalone_sdk/30_tom_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
print("Tom agent consultation example completed!")
print("=" * 80)

# Report cost
cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost}")


# Optional: Index this conversation for Tom's user modeling
# This builds user preferences and patterns from conversation history
Expand Down
82 changes: 82 additions & 0 deletions examples/01_standalone_sdk/31_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import os
import sys

from pydantic import SecretStr

from openhands.sdk import (
LLM,
Agent,
Conversation,
Event,
LLMConvertibleEvent,
get_logger,
)
from openhands.sdk.tool import Tool
from openhands.tools.browser_use import BrowserToolSet
from openhands.tools.file_editor import FileEditorTool


logger = get_logger(__name__)

api_key = os.getenv("LLM_API_KEY")
if api_key is None:
raise RuntimeError("LLM_API_KEY environment variable is not set.")

model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")

llm = LLM(
usage_id="agent",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)

cwd = os.getcwd()
tools = [
Tool(name=FileEditorTool.name),
Tool(name=BrowserToolSet.name),
]

agent = Agent(llm=llm, tools=tools)

llm_messages = []


def _safe_preview(text: str, limit: int = 200) -> str:
truncated = text[:limit]
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
return truncated.encode(encoding, errors="replace").decode(encoding)


def conversation_callback(event: Event) -> None:
if isinstance(event, LLMConvertibleEvent):
llm_messages.append(event.to_llm_message())


conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=cwd,
visualizer=None,
)

try:
conversation.send_message(
"Open https://openhands.dev/blog in the browser and summarize the key points "
"from the latest post."
)
conversation.run()
finally:
conversation.close()

print("=" * 100)
print("Conversation finished. Got the following LLM messages:")
for i, message in enumerate(llm_messages):
preview = _safe_preview(str(message))
print(f"Message {i}: {preview}")

cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost}")
24 changes: 19 additions & 5 deletions openhands-tools/openhands/tools/browser_use/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,12 +560,26 @@ def create(
) -> list[ToolDefinition[BrowserAction, BrowserObservation]]:
# Import executor only when actually needed to
# avoid hanging during module import
from openhands.tools.browser_use.impl import BrowserToolExecutor
import sys

# Use Windows-specific executor on Windows systems
if sys.platform == "win32":
from openhands.tools.browser_use.impl_windows import (
WindowsBrowserToolExecutor,
)

executor = WindowsBrowserToolExecutor(
full_output_save_dir=conv_state.env_observation_persistence_dir,
**executor_config,
)
else:
from openhands.tools.browser_use.impl import BrowserToolExecutor

executor = BrowserToolExecutor(
full_output_save_dir=conv_state.env_observation_persistence_dir,
**executor_config,
)

executor = BrowserToolExecutor(
full_output_save_dir=conv_state.env_observation_persistence_dir,
**executor_config,
)
# Each tool.create() returns a Sequence[Self], so we flatten the results
tools: list[ToolDefinition[BrowserAction, BrowserObservation]] = []
for tool_class in [
Expand Down
Loading
Loading