Make CLI pip-installable (#8772)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Robert Brennan 2025-06-03 19:35:14 -04:00 committed by GitHub
parent 5fe7578f45
commit 4aed3944cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 658 additions and 619 deletions

View File

@ -293,7 +293,7 @@ jobs:
- name: Install poetry via pipx - name: Install poetry via pipx
run: pipx install poetry run: pipx install poetry
- name: Install Python dependencies using Poetry - name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0 run: make install-python-dependencies INSTALL_PLAYWRIGHT=0
- name: Run docker runtime tests - name: Run docker runtime tests
run: | run: |
# We install pytest-xdist in order to run tests across CPUs # We install pytest-xdist in order to run tests across CPUs

View File

@ -54,7 +54,7 @@ jobs:
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly. Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry - name: Install Python dependencies using Poetry
run: poetry install --without evaluation run: poetry install --with dev,test,runtime
- name: Configure config.toml for testing with Haiku - name: Configure config.toml for testing with Haiku
env: env:

View File

@ -44,7 +44,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: 'poetry' cache: 'poetry'
- name: Install Python dependencies using Poetry - name: Install Python dependencies using Poetry
run: poetry install --without evaluation run: poetry install --with dev,test,runtime
- name: Build Environment - name: Build Environment
run: make build run: make build
- name: Run Unit Tests - name: Run Unit Tests
@ -71,7 +71,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: 'poetry' cache: 'poetry'
- name: Install Python dependencies using Poetry - name: Install Python dependencies using Poetry
run: poetry install --without evaluation run: poetry install --with dev,test,runtime
- name: Run Windows unit tests - name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py run: poetry run pytest -svv tests/unit/test_windows_bash.py
- name: Run Windows runtime tests with LocalRuntime - name: Run Windows runtime tests with LocalRuntime

View File

@ -151,7 +151,7 @@ install-python-dependencies:
echo "Installing only POETRY_GROUP=${POETRY_GROUP}"; \ echo "Installing only POETRY_GROUP=${POETRY_GROUP}"; \
poetry install --only $${POETRY_GROUP}; \ poetry install --only $${POETRY_GROUP}; \
else \ else \
poetry install; \ poetry install --with dev,test,runtime; \
fi fi
@if [ "${INSTALL_PLAYWRIGHT}" != "false" ] && [ "${INSTALL_PLAYWRIGHT}" != "0" ]; then \ @if [ "${INSTALL_PLAYWRIGHT}" != "false" ] && [ "${INSTALL_PLAYWRIGHT}" != "0" ]; then \
if [ -f "/etc/manjaro-release" ]; then \ if [ -f "/etc/manjaro-release" ]; then \

View File

@ -26,7 +26,7 @@ RUN apt-get update -y \
COPY ./pyproject.toml ./poetry.lock ./ COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR RUN export POETRY_CACHE_DIR && poetry install --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12.3-slim AS openhands-app FROM python:3.12.3-slim AS openhands-app

View File

@ -325,7 +325,7 @@ async def run_session(
return new_session_requested return new_session_requested
async def main(loop: asyncio.AbstractEventLoop) -> None: async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Runs the agent in CLI mode.""" """Runs the agent in CLI mode."""
args = parse_arguments() args = parse_arguments()
@ -417,11 +417,11 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
) )
if __name__ == '__main__': def main():
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
try: try:
loop.run_until_complete(main(loop)) loop.run_until_complete(main_with_loop(loop))
except KeyboardInterrupt: except KeyboardInterrupt:
print('Received keyboard interrupt, shutting down...') print('Received keyboard interrupt, shutting down...')
except ConnectionRefusedError as e: except ConnectionRefusedError as e:
@ -443,3 +443,7 @@ if __name__ == '__main__':
except Exception as e: except Exception as e:
print(f'Error during cleanup: {e}') print(f'Error during cleanup: {e}')
sys.exit(1) sys.exit(1)
if __name__ == '__main__':
main()

1170
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,47 +20,36 @@ packages = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12,<3.14" python = "^3.12,<3.14"
litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272) litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272)
aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13 aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13
google-generativeai = "*" # To use litellm with Gemini Pro API google-generativeai = "*" # To use litellm with Gemini Pro API
google-api-python-client = "^2.164.0" # For Google Sheets API google-api-python-client = "^2.164.0" # For Google Sheets API
google-auth-httplib2 = "*" # For Google Sheets authentication google-auth-httplib2 = "*" # For Google Sheets authentication
google-auth-oauthlib = "*" # For Google Sheets OAuth google-auth-oauthlib = "*" # For Google Sheets OAuth
termcolor = "*" termcolor = "*"
docker = "*" docker = "*"
fastapi = "*" fastapi = "*"
toml = "*" toml = "*"
uvicorn = "*"
types-toml = "*" types-toml = "*"
uvicorn = "*"
numpy = "*" numpy = "*"
json-repair = "*" json-repair = "*"
browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface
html2text = "*" html2text = "*"
e2b = ">=1.0.5,<1.4.0" e2b = ">=1.0.5,<1.4.0"
pexpect = "*" pexpect = "*"
jinja2 = "^3.1.3" jinja2 = "^3.1.3"
python-multipart = "*" python-multipart = "*"
boto3 = "*"
minio = "^7.2.8"
tenacity = ">=8.5,<10.0" tenacity = ">=8.5,<10.0"
zope-interface = "7.2" zope-interface = "7.2"
pathspec = "^0.12.1" pathspec = "^0.12.1"
google-cloud-aiplatform = "*"
anthropic = { extras = [ "vertex" ], version = "*" }
tree-sitter = "^0.24.0"
bashlex = "^0.18"
pyjwt = "^2.9.0" pyjwt = "^2.9.0"
dirhash = "*" dirhash = "*"
python-frontmatter = "^1.1.0"
python-docx = "*"
PyPDF2 = "*"
python-pptx = "*"
pylatexenc = "*"
tornado = "*" tornado = "*"
python-dotenv = "*" python-dotenv = "*"
rapidfuzz = "^3.9.0" rapidfuzz = "^3.9.0"
whatthepatch = "^1.0.6" whatthepatch = "^1.0.6"
protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+ protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+
opentelemetry-api = "1.25.0" opentelemetry-api = "1.25.0"
opentelemetry-exporter-otlp-proto-grpc = "1.25.0" opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
modal = ">=0.66.26,<0.78.0" modal = ">=0.66.26,<0.78.0"
@ -70,14 +59,8 @@ pygithub = "^2.5.0"
joblib = "*" joblib = "*"
openhands-aci = "0.3.0" openhands-aci = "0.3.0"
python-socketio = "^5.11.4" python-socketio = "^5.11.4"
redis = ">=5.2,<7.0"
sse-starlette = "^2.1.3" sse-starlette = "^2.1.3"
psutil = "*" psutil = "*"
stripe = ">=11.5,<13.0"
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
memory-profiler = "^0.61.0"
daytona-sdk = "0.18.1"
python-json-logger = "^3.2.1" python-json-logger = "^3.2.1"
prompt-toolkit = "^3.0.50" prompt-toolkit = "^3.0.50"
poetry = "^2.1.2" poetry = "^2.1.2"
@ -85,6 +68,27 @@ anyio = "4.9.0"
pythonnet = "*" pythonnet = "*"
fastmcp = "^2.5.2" fastmcp = "^2.5.2"
mcpm = "1.12.0" mcpm = "1.12.0"
python-frontmatter = "^1.1.0"
# TODO: Should these go into the runtime group?
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
PyPDF2 = "*"
python-pptx = "*"
pylatexenc = "*"
python-docx = "*"
bashlex = "^0.18"
# TODO: These are integrations that should probably be optional
redis = ">=5.2,<7.0"
minio = "^7.2.8"
daytona-sdk = "0.18.1"
stripe = ">=11.5,<13.0"
google-cloud-aiplatform = "*"
anthropic = { extras = [ "vertex" ], version = "*" }
boto3 = "*"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "0.11.11" ruff = "0.11.11"
@ -93,6 +97,9 @@ pre-commit = "4.2.0"
build = "*" build = "*"
types-setuptools = "*" types-setuptools = "*"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
pytest = "*" pytest = "*"
pytest-cov = "*" pytest-cov = "*"
@ -104,11 +111,18 @@ pandas = "*"
reportlab = "*" reportlab = "*"
gevent = ">=24.2.1,<26.0.0" gevent = ">=24.2.1,<26.0.0"
[tool.poetry.group.runtime]
optional = true
[tool.poetry.group.runtime.dependencies] [tool.poetry.group.runtime.dependencies]
jupyterlab = "*" jupyterlab = "*"
notebook = "*" notebook = "*"
jupyter_kernel_gateway = "*" jupyter_kernel_gateway = "*"
flake8 = "*" flake8 = "*"
memory-profiler = "^0.61.0"
[tool.poetry.group.evaluation]
optional = true
[tool.poetry.group.evaluation.dependencies] [tool.poetry.group.evaluation.dependencies]
streamlit = "*" streamlit = "*"
@ -132,6 +146,7 @@ browsergym-visualwebarena = "0.13.3"
boto3-stubs = { extras = [ "s3" ], version = "^1.37.19" } boto3-stubs = { extras = [ "s3" ], version = "^1.37.19" }
pyarrow = "20.0.0" # transitive dependency, pinned here to avoid conflicts pyarrow = "20.0.0" # transitive dependency, pinned here to avoid conflicts
datasets = "*" datasets = "*"
joblib = "*"
[tool.poetry.scripts] [tool.poetry.scripts]
openhands = "openhands.cli.main:main" openhands = "openhands.cli.main:main"

View File

@ -1381,6 +1381,7 @@ async def test_first_user_message_with_identical_content(test_event_stream, mock
await controller.close() await controller.close()
@pytest.mark.asyncio
async def test_agent_controller_processes_null_observation_with_cause(): async def test_agent_controller_processes_null_observation_with_cause():
"""Test that AgentController processes NullObservation events with a cause value. """Test that AgentController processes NullObservation events with a cause value.
@ -1395,6 +1396,9 @@ async def test_agent_controller_processes_null_observation_with_cause():
# Create a mock agent with necessary attributes # Create a mock agent with necessary attributes
mock_agent = MagicMock(spec=Agent) mock_agent = MagicMock(spec=Agent)
mock_agent.get_system_message = MagicMock(
return_value=None,
)
mock_agent.llm = MagicMock(spec=LLM) mock_agent.llm = MagicMock(spec=LLM)
mock_agent.llm.metrics = Metrics() mock_agent.llm.metrics = Metrics()
mock_agent.llm.config = OpenHandsConfig().get_llm_config() mock_agent.llm.config = OpenHandsConfig().get_llm_config()
@ -1408,14 +1412,14 @@ async def test_agent_controller_processes_null_observation_with_cause():
) )
# Patch the controller's step method to track calls # Patch the controller's step method to track calls
with patch.object(controller, 'step') as mock_step: with patch.object(controller, '_step') as mock_step:
# Create and add the first user message (will have ID 0) # Create and add the first user message (will have ID 0)
user_message = MessageAction(content='First user message') user_message = MessageAction(content='First user message')
user_message._source = EventSource.USER # type: ignore[attr-defined] user_message._source = EventSource.USER # type: ignore[attr-defined]
event_stream.add_event(user_message, EventSource.USER) event_stream.add_event(user_message, EventSource.USER)
# Give it a little time to process # Give it a little time to process
await asyncio.sleep(0.3) await asyncio.sleep(1)
# Get all events from the stream # Get all events from the stream
events = list(event_stream.get_events()) events = list(event_stream.get_events())

View File

@ -381,7 +381,7 @@ async def test_main_without_task(
mock_run_session.return_value = False mock_run_session.return_value = False
# Run the function # Run the function
await cli.main(loop) await cli.main_with_loop(loop)
# Assertions # Assertions
mock_parse_args.assert_called_once() mock_parse_args.assert_called_once()
@ -458,7 +458,7 @@ async def test_main_with_task(
mock_run_session.side_effect = [True, False] mock_run_session.side_effect = [True, False]
# Run the function # Run the function
await cli.main(loop) await cli.main_with_loop(loop)
# Assertions # Assertions
mock_parse_args.assert_called_once() mock_parse_args.assert_called_once()
@ -553,7 +553,7 @@ async def test_main_with_session_name_passes_name_to_run_session(
mock_run_session.return_value = False mock_run_session.return_value = False
# Run the function # Run the function
await cli.main(loop) await cli.main_with_loop(loop)
# Assertions # Assertions
mock_parse_args.assert_called_once() mock_parse_args.assert_called_once()
@ -713,7 +713,7 @@ async def test_main_security_check_fails(
mock_check_security.return_value = False mock_check_security.return_value = False
# Run the function # Run the function
await cli.main(loop) await cli.main_with_loop(loop)
# Assertions # Assertions
mock_parse_args.assert_called_once() mock_parse_args.assert_called_once()
@ -796,7 +796,7 @@ async def test_config_loading_order(
mock_run_session.return_value = False # No new session requested mock_run_session.return_value = False # No new session requested
# Run the function # Run the function
await cli.main(loop) await cli.main_with_loop(loop)
# Assertions for argument parsing and config setup # Assertions for argument parsing and config setup
mock_parse_args.assert_called_once() mock_parse_args.assert_called_once()