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
run: pipx install 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
run: |
# 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.
- 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
env:

View File

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

View File

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

View File

@ -26,7 +26,7 @@ RUN apt-get update -y \
COPY ./pyproject.toml ./poetry.lock ./
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

View File

@ -325,7 +325,7 @@ async def run_session(
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."""
args = parse_arguments()
@ -417,11 +417,11 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
)
if __name__ == '__main__':
def main():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main(loop))
loop.run_until_complete(main_with_loop(loop))
except KeyboardInterrupt:
print('Received keyboard interrupt, shutting down...')
except ConnectionRefusedError as e:
@ -443,3 +443,7 @@ if __name__ == '__main__':
except Exception as e:
print(f'Error during cleanup: {e}')
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]
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)
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-api-python-client = "^2.164.0" # For Google Sheets API
google-auth-httplib2 = "*" # For Google Sheets authentication
google-auth-oauthlib = "*" # For Google Sheets OAuth
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
google-generativeai = "*" # To use litellm with Gemini Pro API
google-api-python-client = "^2.164.0" # For Google Sheets API
google-auth-httplib2 = "*" # For Google Sheets authentication
google-auth-oauthlib = "*" # For Google Sheets OAuth
termcolor = "*"
docker = "*"
fastapi = "*"
toml = "*"
uvicorn = "*"
types-toml = "*"
uvicorn = "*"
numpy = "*"
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 = "*"
e2b = ">=1.0.5,<1.4.0"
pexpect = "*"
jinja2 = "^3.1.3"
python-multipart = "*"
boto3 = "*"
minio = "^7.2.8"
tenacity = ">=8.5,<10.0"
zope-interface = "7.2"
pathspec = "^0.12.1"
google-cloud-aiplatform = "*"
anthropic = { extras = [ "vertex" ], version = "*" }
tree-sitter = "^0.24.0"
bashlex = "^0.18"
pyjwt = "^2.9.0"
dirhash = "*"
python-frontmatter = "^1.1.0"
python-docx = "*"
PyPDF2 = "*"
python-pptx = "*"
pylatexenc = "*"
tornado = "*"
python-dotenv = "*"
rapidfuzz = "^3.9.0"
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-exporter-otlp-proto-grpc = "1.25.0"
modal = ">=0.66.26,<0.78.0"
@ -70,14 +59,8 @@ pygithub = "^2.5.0"
joblib = "*"
openhands-aci = "0.3.0"
python-socketio = "^5.11.4"
redis = ">=5.2,<7.0"
sse-starlette = "^2.1.3"
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"
prompt-toolkit = "^3.0.50"
poetry = "^2.1.2"
@ -85,6 +68,27 @@ anyio = "4.9.0"
pythonnet = "*"
fastmcp = "^2.5.2"
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]
ruff = "0.11.11"
@ -93,6 +97,9 @@ pre-commit = "4.2.0"
build = "*"
types-setuptools = "*"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
pytest = "*"
pytest-cov = "*"
@ -104,11 +111,18 @@ pandas = "*"
reportlab = "*"
gevent = ">=24.2.1,<26.0.0"
[tool.poetry.group.runtime]
optional = true
[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
jupyter_kernel_gateway = "*"
flake8 = "*"
memory-profiler = "^0.61.0"
[tool.poetry.group.evaluation]
optional = true
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
@ -132,6 +146,7 @@ browsergym-visualwebarena = "0.13.3"
boto3-stubs = { extras = [ "s3" ], version = "^1.37.19" }
pyarrow = "20.0.0" # transitive dependency, pinned here to avoid conflicts
datasets = "*"
joblib = "*"
[tool.poetry.scripts]
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()
@pytest.mark.asyncio
async def test_agent_controller_processes_null_observation_with_cause():
"""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
mock_agent = MagicMock(spec=Agent)
mock_agent.get_system_message = MagicMock(
return_value=None,
)
mock_agent.llm = MagicMock(spec=LLM)
mock_agent.llm.metrics = Metrics()
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
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)
user_message = MessageAction(content='First user message')
user_message._source = EventSource.USER # type: ignore[attr-defined]
event_stream.add_event(user_message, EventSource.USER)
# Give it a little time to process
await asyncio.sleep(0.3)
await asyncio.sleep(1)
# Get all events from the stream
events = list(event_stream.get_events())

View File

@ -381,7 +381,7 @@ async def test_main_without_task(
mock_run_session.return_value = False
# Run the function
await cli.main(loop)
await cli.main_with_loop(loop)
# Assertions
mock_parse_args.assert_called_once()
@ -458,7 +458,7 @@ async def test_main_with_task(
mock_run_session.side_effect = [True, False]
# Run the function
await cli.main(loop)
await cli.main_with_loop(loop)
# Assertions
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
# Run the function
await cli.main(loop)
await cli.main_with_loop(loop)
# Assertions
mock_parse_args.assert_called_once()
@ -713,7 +713,7 @@ async def test_main_security_check_fails(
mock_check_security.return_value = False
# Run the function
await cli.main(loop)
await cli.main_with_loop(loop)
# Assertions
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
# Run the function
await cli.main(loop)
await cli.main_with_loop(loop)
# Assertions for argument parsing and config setup
mock_parse_args.assert_called_once()