feat: add --extra-python-path / OH_EXTRA_PYTHON_PATH for custom tool imports in binary builds#3240
feat: add --extra-python-path / OH_EXTRA_PYTHON_PATH for custom tool imports in binary builds#3240xingyaoww wants to merge 8 commits into
Conversation
…imports in binary builds Custom tools fail to import in PyInstaller binary builds because the frozen importer cannot find external .py files outside the bundled archive. This adds a mechanism to extend sys.path at startup so importlib.import_module() can locate third-party tool modules. Changes: - Add extend_python_path() function that reads directories from the --extra-python-path CLI arg and/or OH_EXTRA_PYTHON_PATH env var, validates they exist, deduplicates, and prepends to sys.path - Wire up the call in main() before preload_modules() so paths are available before any tool registration - Add 11 tests covering noop, CLI, env var, merge, dedup, nonexistent dir warning, and end-to-end integration with preload_modules Tested with PyInstaller binary: external .py tool files (including ones that import from openhands.sdk and call register_tool()) load successfully. Fixes #1531 Co-authored-by: openhands <openhands@all-hands.dev>
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ Solid solution for enabling custom tools in PyInstaller builds. Comprehensive test coverage and proper validation. One minor grammar fix suggested inline.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
All functional verification passed. The PR successfully enables custom tool imports in both source and binary builds through --extra-python-path CLI flag and OH_EXTRA_PYTHON_PATH environment variable.
Does this PR achieve its stated goal?
Yes. The PR solves the problem of custom tools failing to import in PyInstaller binary builds. I verified that:
- Without the fix, external modules cannot be imported (baseline confirmed)
- With
--extra-python-path, directories are added tosys.pathand modules import successfully - With
OH_EXTRA_PYTHON_PATH, the same functionality works via environment variable - Both sources merge correctly when used together
- Integration with
preload_modules()works as intended - The agent-server starts successfully and imports custom modules with both mechanisms
| Phase | Result |
|---|---|
| Environment Setup | ✅ make build completed successfully |
| CI Status | ✅ All checks passing (build-binary-and-test on ubuntu/macos/windows, sdk-tests, pre-commit, API checks) |
| Functional Verification | ✅ All 8 test scenarios passed |
Functional Verification
Test 1: Baseline — Reproduce the Problem
Step 1 — Verify module cannot be imported without extra path:
Created test module /tmp/test_custom_tools/my_custom_tool.py with constants and functions.
Ran Python import attempt:
import my_custom_toolOutput:
ModuleNotFoundError: No module named 'my_custom_tool'
This confirms the baseline problem: external modules are not on sys.path and cannot be imported.
Test 2: CLI Argument (--extra-python-path)
Step 1 — Apply the fix with CLI arg:
Called extend_python_path("/tmp/test_custom_tools")
Output:
Added to sys.path: /tmp/test_custom_tools
Extended sys.path with 1 directory for custom tool imports
Step 2 — Verify module imports successfully:
Ran:
import my_custom_tool
print(my_custom_tool.TOOL_NAME)Output:
✅ SUCCESS: Imported MyCustomTool
TOOL_REGISTERED = True
Function call result: Custom tool executed: test
The module imported successfully and functions are callable.
Test 3: Environment Variable (OH_EXTRA_PYTHON_PATH)
Step 1 — Apply the fix with env var:
Set OH_EXTRA_PYTHON_PATH=/tmp/test_custom_tools and called extend_python_path(None)
Output:
Added to sys.path: /tmp/test_custom_tools
Extended sys.path with 1 directory for custom tool imports
Step 2 — Verify module imports:
Output:
✅ SUCCESS: Imported MyCustomTool
TOOL_REGISTERED = True
Environment variable mechanism works correctly.
Test 4: Merging Both Sources
Setup: Created second test module in /tmp/test_custom_tools_2/another_tool.py
Step 1 — Apply both CLI arg and env var:
Set OH_EXTRA_PYTHON_PATH=/tmp/test_custom_tools_2 and called extend_python_path("/tmp/test_custom_tools")
Output:
Added to sys.path: /tmp/test_custom_tools
Added to sys.path: /tmp/test_custom_tools_2
Extended sys.path with 2 directory for custom tool imports
Step 2 — Verify both modules import:
Output:
✅ SUCCESS: Imported MyCustomTool (from CLI arg path)
✅ SUCCESS: Imported AnotherCustomTool (from env var path)
Both sources merge correctly.
Test 5: Integration with preload_modules()
Step 1 — Extend path then preload:
Called extend_python_path("/tmp/test_custom_tools") then preload_modules("my_custom_tool")
Output:
Added to sys.path: /tmp/test_custom_tools
Extended sys.path with 1 directory for custom tool imports
Imported module: my_custom_tool
✅ SUCCESS: Module preloaded via preload_modules()
TOOL_NAME = MyCustomTool
TOOL_REGISTERED = True
The intended workflow (extend_python_path → preload_modules) works as designed.
Test 6: Agent-Server CLI with --extra-python-path
Command:
python -m openhands.agent_server --port 18765 --extra-python-path /tmp/test_custom_tools --import-modules my_custom_toolOutput:
Added to sys.path: /tmp/test_custom_tools
Extended sys.path with 1 directory for custom tool imports
Imported module: my_custom_tool
Started server process [4012]
Agent-server starts successfully and loads the custom module from the external path.
Test 7: Agent-Server with OH_EXTRA_PYTHON_PATH
Command:
OH_EXTRA_PYTHON_PATH=/tmp/test_custom_tools python -m openhands.agent_server --port 18766 --import-modules my_custom_toolOutput:
Added to sys.path: /tmp/test_custom_tools
Extended sys.path with 1 directory for custom tool imports
Imported module: my_custom_tool
Started server process [4064]
Environment variable works with the actual agent-server.
Test 8: Non-Existent Directory Warning
Step 1 — Call with non-existent path:
Called extend_python_path("/tmp/this_does_not_exist_xyz123")
Output:
WARNING: Ignoring non-existent --extra-python-path entry: /tmp/this_does_not_exist_xyz123
✅ SUCCESS: Non-existent directory was skipped
The function correctly warns and skips non-existent directories without crashing.
Test 9: CLI Help Text
Command:
python -m openhands.agent_server --helpOutput includes:
--extra-python-path EXTRA_PYTHON_PATH
Additional directories to add to sys.path for custom
tool imports (':'-separated). Also reads from the
OH_EXTRA_PYTHON_PATH environment variable.
CLI flag is properly documented in help text.
Issues Found
None.
After building the agent-server binary, run three smoke tests: 1. Negative test: verify import fails WITHOUT --extra-python-path (confirms the binary can't find external .py files by default) 2. Positive test (env var): verify import succeeds when OH_EXTRA_PYTHON_PATH points to a directory containing an external tool module 3. Positive test (CLI flag): verify import succeeds when --extra-python-path is passed on the command line Each test creates a temporary ci_test_tool.py file, launches the binary in the background, waits for startup, and checks log output for the expected import success/failure messages. Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
…hub.com/OpenHands/software-agent-sdk into feat/extra-python-path-for-custom-tools
|
@OpenHands https://github.com/OpenHands/software-agent-sdk/actions/runs/25820768480/job/75861100352?pr=3240 Can you help me fix the CI here and help me create a new pull request on the openhands/docs folder repo? We basically want to showcase to users how you can use the OpenHands agent server while bringing your own custom tools. |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
|
Fixed the linked Windows CI failure and opened the docs PR. What changed here:
Validation:
Docs PR: OpenHands/docs#505 This comment was created by an AI agent (OpenHands) on behalf of the user. |
|
Since my last summary, there were no additional code or docs changes. Final status:
|
Co-authored-by: openhands <openhands@all-hands.dev>
Problem
Custom tools fail to import in PyInstaller binary builds (issue #1531). The frozen importer cannot find external
.pyfiles outside the bundled archive, soimportlib.import_module()raisesModuleNotFoundErrorfor anytool_module_qualnamespointing to third-party tool code.Solution
Add
extend_python_path()to prepend extra directories tosys.pathat agent-server startup, beforepreload_modules()runs. Directories are specified via:--extra-python-path /path/to/tools:/another/pathOH_EXTRA_PYTHON_PATH=/path/to/tools:/another/pathBoth sources are merged. Non-existent directories are skipped with a warning; duplicates and paths already on
sys.pathare silently ignored.Changes
openhands-agent-server/openhands/agent_server/__main__.py_EXTRA_PYTHON_PATH_ENVconstantextend_python_path(extra_paths)function with validation, dedup, and logging--extra-python-pathargparse argumentextend_python_path()beforepreload_modules()inmain()tests/agent_server/test_preload_modules.pyTestExtendPythonPathclass with 11 test methods covering:.pymoduleextend_python_path()thenpreload_modules().github/workflows/server.ymlbuild-binary-and-testjob--extra-python-path(confirms the problem exists)OH_EXTRA_PYTHON_PATHpoints to external tool dir--extra-python-pathis passed on the command lineTesting
All 27 unit tests pass (16 existing + 11 new).
Binary build verification (PyInstaller
agent-server.spec):OH_EXTRA_PYTHON_PATH:ModuleNotFoundError: No module named 'my_external_tool'(confirms the problem)OH_EXTRA_PYTHON_PATH=/tmp/test_external_tools:Imported module: my_external_tool(confirms the fix)from openhands.sdkand callingregister_tool(): loads successfully from the frozen binaryFixes #1531
This PR was created by an AI agent (OpenHands) on behalf of the user.
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:6357d5c-pythonRun
All tags pushed for this build
About Multi-Architecture Support
6357d5c-python) is a multi-arch manifest supporting both amd64 and arm646357d5c-python-amd64) are also available if needed