Compare commits

...

27 Commits

Author SHA1 Message Date
sp.wack 642e01b673 fix(frontend): Update build directory and referenced paths (#4461) 2024-10-17 23:24:49 +04:00
sp.wack 6cb174b7d1 [ALL-557] feat(frontend): Add save and discard actions to the editor (#4442)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2024-10-17 17:14:55 +00:00
Robert Brennan 154854bbe3 run in dev mode in makefile (#4452) 2024-10-17 12:40:47 -04:00
sp.wack 678630c5bd fix(frontend): Catch config fetch error and set default fallback (#4453) 2024-10-17 16:17:44 +00:00
dependabot[bot] ad800bf373 chore(deps): bump litellm from 1.49.5 to 1.49.6 (#4458)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 17:51:45 +02:00
dependabot[bot] 206788a0e8 chore(deps): bump react-syntax-highlighter from 15.5.0 to 15.6.1 in /frontend (#4457)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 15:31:07 +00:00
dependabot[bot] ca3fbb2a80 chore(deps-dev): bump @types/node from 22.7.5 to 22.7.6 in /frontend (#4455)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 15:29:23 +00:00
dependabot[bot] cc500a622a chore(deps-dev): bump @testing-library/jest-dom from 6.5.0 to 6.6.1 in /frontend (#4456)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 15:29:01 +00:00
tofarr 5fb3dece93 Feat: Divided docker layer to make it easier to cache (#4313)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-10-17 15:08:56 +00:00
sp.wack 83c096b974 [ALL-551] chore(frontend): Retrieve APP_MODE from the server (#4423) 2024-10-17 18:35:21 +04:00
Xingyao Wang 015df47e53 chore: remove integration tests from CI to unblock (#4451) 2024-10-17 14:19:53 +00:00
Jiayi Pan c1b323a076 Show actual dataset name in swebench log directory (#4417) 2024-10-17 10:32:38 +08:00
Xingyao Wang 84a578ad20 [test] remove integration tests from CI & move them into evaluation (#4447) 2024-10-17 05:38:23 +08:00
dependabot[bot] 8e5db345b2 chore(deps): bump boto3 from 1.35.40 to 1.35.42 (#4445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 22:51:40 +02:00
dependabot[bot] f61266841c chore(deps): bump browsergym from 0.8.0 to 0.8.1 (#4437)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 22:50:39 +02:00
dependabot[bot] 277d991b37 chore(deps): bump fastapi from 0.115.0 to 0.115.2 (#4370)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 22:02:26 +02:00
Engel Nyst 20aa66d5e2 Bump Mac version in CI (#4441) 2024-10-16 21:52:21 +02:00
dependabot[bot] 9bc6252967 chore(deps): bump anthropic from 0.36.0 to 0.36.1 (#4436)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 21:25:00 +02:00
Alejandro Cuadron Lafuente bb416009c5 [Fix] Fixed the inputs to the ManagerAgent (#4427)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Robert Brennan <contact@rbren.io>
2024-10-16 20:47:46 +02:00
Robert Brennan 226ea545fa Add workflow scope to GitHub authentication URL (#4439)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-10-16 14:41:46 -04:00
tofarr e12bff5189 Fix: Removed flaky test (#4444) 2024-10-16 18:10:27 +00:00
dependabot[bot] 23d3becf1d chore(deps): bump litellm from 1.49.4 to 1.49.5 (#4431)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 18:36:39 +02:00
Robert Brennan be79ccdb39 fix default host (#4413) 2024-10-16 10:56:42 -04:00
sp.wack 2277897f86 feat(frontend): Improve file based routing (#4317) 2024-10-16 18:54:15 +04:00
tofarr be9619be3a Feat faster unit tests 2 (#4418) 2024-10-16 08:40:53 -06:00
tofarr cb58dab82b Fix loop graceful shutdown (#4394) 2024-10-16 08:40:33 -06:00
sp.wack 8ab293a667 fix(frontend): Fix request headers (#4422) 2024-10-16 14:22:18 +00:00
256 changed files with 1818 additions and 33451 deletions
+2 -74
View File
@@ -384,78 +384,6 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Run integration tests with the eventstream runtime Docker image
runtime_integration_tests_on_linux:
name: RT Integration Tests (Linux)
runs-on: ubuntu-latest
needs: [ghcr_build_runtime]
strategy:
fail-fast: false
matrix:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-${{ matrix.base_image }}
path: /tmp
- name: Load runtime image for fork
if: github.event.pull_request.head.repo.fork
run: |
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Run integration tests
run: |
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
TEST_ONLY=true \
./tests/integration/regenerate.sh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
# "All Runtime Tests Passed" is a required job for PRs to merge
# Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
@@ -464,7 +392,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
@@ -473,7 +401,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: Some tests failed
run: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
# Run python unit tests on macOS
test-on-macos:
name: Python Unit Tests on macOS
runs-on: macos-12
runs-on: macos-14
env:
INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
strategy:
@@ -1,73 +0,0 @@
name: Regenerate Integration Tests
on:
workflow_dispatch:
inputs:
debug:
description: 'Enable debug mode'
type: boolean
default: true
log_to_file:
description: 'Enable logging to file'
type: boolean
default: true
force_regenerate_tests:
description: 'Force regeneration of tests'
type: boolean
default: false
force_use_llm:
description: 'Force use of LLM'
type: boolean
default: false
jobs:
regenerate_integration_tests:
if: github.ref != 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Build Environment
run: make build
- name: Regenerate integration tests
run: |
DEBUG=${{ inputs.debug }} \
LOG_TO_FILE=${{ inputs.log_to_file }} \
FORCE_REGENERATE=${{ inputs.force_regenerate_tests }} \
FORCE_USE_LLM=${{ inputs.force_use_llm }} \
./tests/integration/regenerate.sh
- name: Commit changes
run: |
if git diff --quiet --exit-code; then
echo "No changes to commit"
exit 0
fi
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add .
# run it twice in case pre-commit makes changes
git commit -am "Regenerate integration tests" || git commit -am "Regenerate integration tests"
git push
-3
View File
@@ -91,9 +91,6 @@ To run tests, refer to the following:
poetry run pytest ./tests/unit/test_*.py
```
#### Integration tests
Please refer to [this README](./tests/integration/README.md) for details.
### 9. Add or update dependency
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`
2. Update the poetry.lock file via `poetry lock --no-update`
+2 -2
View File
@@ -195,7 +195,7 @@ start-backend:
# Start frontend
start-frontend:
@echo "$(YELLOW)Starting frontend...$(RESET)"
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start -- --port $(FRONTEND_PORT)
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run dev -- --port $(FRONTEND_PORT)
# Common setup for running the app (non-callable)
_run_setup:
@@ -214,7 +214,7 @@ _run_setup:
run:
@echo "$(YELLOW)Running the app...$(RESET)"
@$(MAKE) -s _run_setup
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT)
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run dev -- --port $(FRONTEND_PORT)
@echo "$(GREEN)Application started successfully.$(RESET)"
# Run the app (in docker)
+2 -2
View File
@@ -8,7 +8,7 @@ RUN npm install -g npm@10.5.1
RUN npm ci
COPY ./frontend ./
RUN npm run make-i18n && npm run build
RUN npm run build
FROM python:3.12.3-slim AS backend-builder
@@ -90,7 +90,7 @@ RUN python openhands/core/download.py # No-op to download assets
# openhands:openhands -> openhands:app
RUN find /app \! -group app -exec chgrp app {} +
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build/client ./frontend/build
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build
COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
USER root
+27 -55
View File
@@ -70,74 +70,46 @@ Check out the [relevant code](https://github.com/All-Hands-AI/OpenHands/blob/mai
### Image Tagging System
OpenHands uses a dual-tagging system for its runtime images to balance reproducibility with flexibility:
OpenHands uses a dual-tagging system for its runtime images to balance reproducibility with flexibility.
Tags may be in one of 2 formats:
1. Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`.
Example: `runtime:abc123def456`
- **Generic**: `oh_v{openhands_version}_{16_digit_lock_hash}` (e.g.: `oh_v0.9.9_1234567890abcdef`)
- **Specific**: `oh_v{openhands_version}_{16_digit_lock_hash}_{16_digit_source_hash}`
(e.g.: `oh_v0.9.9_1234567890abcdef_1234567890abcdef`)
- This tag is based on the MD5 hash of the Docker build folder, which includes the source code (of runtime client and related dependencies) and Dockerfile
- Identical hash tags guarantee that the images were built with exactly the same source code and Dockerfile
- This ensures reproducibility; the same hash always means the same image contents
#### Lock Hash
2. Generic tag: `{target_image_repo}:{target_image_tag}`.
Example: `runtime:oh_v0.9.3_ubuntu_tag_22.04`
This hash is built from the first 16 digits of the MD5 of:
- The name of the base image upon which the image was built (e.g.: `nikolaik/python-nodejs:python3.12-nodejs22`)
- The content of the `pyproject.toml` included in the image.
- The content of the `poetry.lock` included in the image.
- This tag follows the format: `runtime:oh_v{OH_VERSION}_{BASE_IMAGE_NAME}_tag_{BASE_IMAGE_TAG}`
- It represents the latest build for a particular base image and OpenHands version combination
- This tag is updated whenever a new image is built from the same base image, even if the source code changes
This effectively gives a hash for the dependencies of Openhands independent of the source code.
The hash-based tag ensures reproducibility, while the generic tag provides a stable reference to the latest version of a particular configuration. This dual-tagging approach allows OpenHands to efficiently manage both development and production environments.
#### Source Hash
### Build Process
This is the first 16 digits of the MD5 of the directory hash for the source directory. This gives a hash
for only the openhands source
1. Image Naming Convention:
- Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`.
Example: `runtime:abc123def456`
- Generic tag: `{target_image_repo}:{target_image_tag}`.
Example: `runtime:oh_v0.9.3_ubuntu_tag_22.04`
#### Build Process
2. Build Process:
- a. Convert the base image name to an OH runtime image name
Example: `ubuntu:22.04` -> `runtime:oh_v0.9.3_ubuntu_tag_22.04`
- b. Generate a build context (Dockerfile and OpenHands source code) and calculate its hash
- c. Check for an existing image with the calculated hash
- d. If not found, check for a recent compatible image to use as a base
- e. If no compatible image exists, build from scratch using the original base image
- f. Tag the new image with both hash-based and generic tags
When generating an image...
3. Image Reuse and Rebuilding Logic:
The system follows these steps to determine whether to build a new image or use an existing one from a user-provided (base) image (e.g., `ubuntu:22.04`):
- a. If an image exists with the same hash (e.g., `runtime:abc123def456`), it will be reused as is
- b. If the exact hash is not found, the system will try to rebuild using the latest generic image (e.g., `runtime:oh_v0.9.3_ubuntu_tag_22.04`) as a base. This saves time by leveraging existing dependencies
- c. If neither the hash-tagged nor the generic-tagged image is found, the system will build the image completely from scratch
- OpenHands first checks whether an image with the same **Specific** tag exists. If there is such an image,
no build is performed - the existing image is used.
- OpenHands next checks whether an image with the **Generic** tag exists. If there is such an image,
OpenHands builds a new image based upon it, bypassing all installation steps (like `poetry install` and
`apt-get`) except a final operation to copy the current source code. The new image is tagged with a
**Specific** tag only.
- If neither a **Specific** nor **Generic** tag exists, a brand new image is built based upon the base
image (Which is a slower operation). This new image is tagged with both the **Generic** and **Specific**
tags.
4. Caching and Efficiency:
- The system attempts to reuse existing images when possible to save build time
- If an exact match (by hash) is found, it's used without rebuilding
- If a compatible image is found, it's used as a base for rebuilding, saving time on dependency installation
Here's a flowchart illustrating the build process:
```mermaid
flowchart TD
A[Start] --> B{Convert base image name}
B --> |ubuntu:22.04 -> runtime:oh_v0.9.3_ubuntu_tag_22.04| C[Generate build context and hash]
C --> D{Check for existing image with hash}
D -->|Found runtime:abc123def456| E[Use existing image]
D -->|Not found| F{Check for runtime:oh_v0.9.3_ubuntu_tag_22.04}
F -->|Found| G[Rebuild based on recent image]
F -->|Not found| H[Build from scratch]
G --> I[Tag with hash and generic tags]
H --> I
E --> J[End]
I --> J
```
This approach ensures that:
This dual-tagging approach allows OpenHands to efficiently manage both development and production environments.
1. Identical source code and Dockerfile always produce the same image (via hash-based tags)
2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images)
3. The generic tag (e.g., `runtime:oh_v0.9.3_ubuntu_tag_22.04`) always points to the latest build for a particular base image and OpenHands version combination
3. The generic tag (e.g., `runtime:oh_v0.9.3_1234567890abcdef`) always points to the latest build for a particular base image and OpenHands version combination
## Runtime Plugin System
+69
View File
@@ -0,0 +1,69 @@
# Integration tests
This directory implements integration tests that [was running in CI](https://github.com/All-Hands-AI/OpenHands/tree/23d3becf1d6f5d07e592f7345750c314a826b4e9/tests/integration).
[PR 3985](https://github.com/All-Hands-AI/OpenHands/pull/3985) introduce LLM-based editing, which requires access to LLM to perform edit. Hence, we remove integration tests from CI and intend to run them as nightly evaluation to ensure the quality of OpenHands softwares.
## To add new tests
Each test is a file named like `tXX_testname.py` where `XX` is a number.
Make sure to name the file for each test to start with `t` and ends with `.py`.
Each test should be structured as a subclass of [`BaseIntegrationTest`](./tests/base.py), where you need to implement `initialize_runtime` that setup the runtime enviornment before test, and `verify_result` that takes in a `Runtime` and history of `Event` and return a `TestResult`. See [t01_fix_simple_typo.py](./tests/t01_fix_simple_typo.py) and [t05_simple_browsing.py](./tests/t05_simple_browsing.py) for two representative examples.
```python
class TestResult(BaseModel):
success: bool
reason: str | None = None
class BaseIntegrationTest(ABC):
"""Base class for integration tests."""
INSTRUCTION: str
@classmethod
@abstractmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
"""Initialize the runtime for the test to run."""
pass
@classmethod
@abstractmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
"""Verify the result of the test.
This method will be called after the agent performs the task on the runtime.
"""
pass
```
## Setup Environment and LLM Configuration
Please follow instruction [here](../README.md#setup) to setup your local
development environment and LLM.
## Start the evaluation
```bash
./evaluation/integration_tests/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [eval-num-workers] [eval_ids]
```
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for
your LLM settings, as defined in your `config.toml`.
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version
you would like to evaluate. It could also be a release tag like `0.9.0`.
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks,
defaulting to `CodeActAgent`.
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit`
instances. By default, the script evaluates the entire Exercism test set
(133 issues). Note: in order to use `eval_limit`, you must also set `agent`.
- `eval-num-workers`: the number of workers to use for evaluation. Default: `1`.
- `eval_ids`, e.g. `"1,3,10"`, limits the evaluation to instances with the
given IDs (comma separated).
Example:
```bash
./evaluation/integration_tests/scripts/run_infer.sh llm.claude-35-sonnet-eval HEAD CodeActAgent
```
+202
View File
@@ -0,0 +1,202 @@
import asyncio
import importlib.util
import os
import pandas as pd
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
SandboxConfig,
get_llm_config_arg,
parse_arguments,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.runtime.runtime import Runtime
FAKE_RESPONSES = {
'CodeActAgent': codeact_user_response,
}
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
# use default base_container_image
enable_auto_lint=True,
use_host_network=False,
timeout=100,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
return config
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
config = get_config(metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, str(instance.instance_id), log_dir)
else:
logger.info(
f'\nStarting evaluation for instance {str(instance.instance_id)}.\n'
)
# =============================================
# import test instance
# =============================================
instance_id = instance.instance_id
spec = importlib.util.spec_from_file_location(instance_id, instance.file_path)
test_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_module)
assert hasattr(
test_module, 'Test'
), f'Test module {instance_id} does not have a Test class'
test_class: type[BaseIntegrationTest] = test_module.Test
assert issubclass(
test_class, BaseIntegrationTest
), f'Test class {instance_id} does not inherit from BaseIntegrationTest'
instruction = test_class.INSTRUCTION
# =============================================
# create sandbox and run the agent
# =============================================
runtime: Runtime = create_runtime(config)
test_class.initialize_runtime(runtime)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=FAKE_RESPONSES[metadata.agent_class],
)
)
if state is None:
raise ValueError('State should not be None.')
# # =============================================
# # result evaluation
# # =============================================
histories = state.history.get_events()
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = state.metrics.get() if state.metrics else None
# Save the output
output = EvalOutput(
instance_id=str(instance.instance_id),
instance=instance.to_dict(),
instruction=instruction,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result=test_result.model_dump(),
)
return output
def load_integration_tests() -> pd.DataFrame:
"""Load tests from python files under ./tests"""
cur_dir = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(cur_dir, 'tests')
test_files = [
os.path.join(test_dir, f)
for f in os.listdir(test_dir)
if f.startswith('t') and f.endswith('.py')
]
df = pd.DataFrame(test_files, columns=['file_path'])
df['instance_id'] = df['file_path'].apply(
lambda x: os.path.basename(x).rstrip('.py')
)
return df
if __name__ == '__main__':
args = parse_arguments()
integration_tests = load_integration_tests()
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
'integration_tests',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
# Parse dataset IDs if provided
eval_ids = None
if args.eval_ids:
eval_ids = str(args.eval_ids).split(',')
logger.info(f'\nUsing specific dataset IDs: {eval_ids}\n')
instances = prepare_dataset(
integration_tests,
output_file,
args.eval_n_limit,
eval_ids=eval_ids,
)
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
)
df = pd.read_json(output_file, lines=True, orient='records')
df['success'] = df['test_result'].apply(lambda x: x['success'])
df['reason'] = df['test_result'].apply(lambda x: x['reason'])
logger.info('-' * 100)
logger.info(
f'Success rate: {df["success"].mean():.2%} ({df["success"].sum()}/{len(df)})'
)
logger.info(
'\nEvaluation Results:'
+ '\n'
+ df[['instance_id', 'success', 'reason']].to_string(index=False)
)
logger.info('-' * 100)
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
NUM_WORKERS=$5
EVAL_IDS=$6
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
get_agent_version
echo "AGENT: $AGENT"
echo "AGENT_VERSION: $AGENT_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
EVAL_NOTE=$AGENT_VERSION
# Default to NOT use unit tests.
if [ -z "$USE_UNIT_TESTS" ]; then
export USE_UNIT_TESTS=false
fi
echo "USE_UNIT_TESTS: $USE_UNIT_TESTS"
# If use unit tests, set EVAL_NOTE to the commit hash
if [ "$USE_UNIT_TESTS" = true ]; then
EVAL_NOTE=$EVAL_NOTE-w-test
fi
# export PYTHONPATH=evaluation/integration_tests:\$PYTHONPATH
COMMAND="poetry run python evaluation/integration_tests/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \
--eval-num-workers $NUM_WORKERS \
--eval-note $EVAL_NOTE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
if [ -n "$EVAL_IDS" ]; then
echo "EVAL_IDS: $EVAL_IDS"
COMMAND="$COMMAND --eval-ids $EVAL_IDS"
fi
# Run the command
eval $COMMAND
@@ -0,0 +1,32 @@
from abc import ABC, abstractmethod
from pydantic import BaseModel
from openhands.events.event import Event
from openhands.runtime.runtime import Runtime
class TestResult(BaseModel):
success: bool
reason: str | None = None
class BaseIntegrationTest(ABC):
"""Base class for integration tests."""
INSTRUCTION: str
@classmethod
@abstractmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
"""Initialize the runtime for the test to run."""
pass
@classmethod
@abstractmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
"""Verify the result of the test.
This method will be called after the agent performs the task on the runtime.
"""
pass
@@ -0,0 +1,39 @@
import os
import tempfile
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.runtime import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Fix typos in bad.txt.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
# create a file with a typo in /workspace/bad.txt
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, 'bad.txt')
with open(temp_file_path, 'w') as f:
f.write('This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!')
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/workspace')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/bad.txt has been fixed
action = CmdRunAction(command='cat /workspace/bad.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False, reason=f'Failed to run command: {obs.content}'
)
# check if the file /workspace/bad.txt has been fixed
if (
obs.content.strip().replace('\r\n', '\n')
== 'This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!'
):
return TestResult(success=True)
return TestResult(success=False, reason=f'File not fixed: {obs.content}')
@@ -0,0 +1,40 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.runtime import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = "Write a shell script '/workspace/hello.sh' that prints 'hello'."
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/hello.sh', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/hello.sh: {obs.content}.',
)
# execute the script
action = CmdRunAction(command='bash /workspace/hello.sh', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to execute /workspace/hello.sh: {obs.content}.',
)
if obs.content.strip() != 'hello':
return TestResult(
success=False, reason=f'Script did not print "hello": {obs.content}.'
)
return TestResult(success=True)
@@ -0,0 +1,43 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.runtime import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = "Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'."
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
)
# execute the script
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
)
if 'hello world' not in obs.content.strip():
return TestResult(
success=False,
reason=f'File did not contain "hello world": {obs.content}.',
)
return TestResult(success=True)
@@ -0,0 +1,58 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.runtime import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Write a git commit message for the current staging area and commit the changes.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git init
action = CmdRunAction(command='git init', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create README.md
action = CmdRunAction(
command='echo \'print("hello world")\' > hello.py', keep_prompt=False
)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git add README.md
action = CmdRunAction(command='git add hello.py', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.py exists
action = CmdRunAction(command='cat /workspace/hello.py', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/hello.py: {obs.content}.',
)
# check if the staging area is empty
action = CmdRunAction(command='git status', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False, reason=f'Failed to git status: {obs.content}.'
)
if 'nothing to commit, working tree clean' in obs.content.strip():
return TestResult(success=True)
return TestResult(
success=False,
reason=f'Failed to check for "nothing to commit, working tree clean": {obs.content}.',
)
@@ -0,0 +1,134 @@
import os
import tempfile
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.runtime import Runtime
HTML_FILE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Ultimate Answer</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(to right, #1e3c72, #2a5298);
color: #fff;
font-family: 'Arial', sans-serif;
text-align: center;
}
.container {
text-align: center;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
p {
font-size: 18px;
margin-bottom: 30px;
}
#showButton {
padding: 10px 20px;
font-size: 16px;
color: #1e3c72;
background: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s ease;
}
#showButton:hover {
background: #f0f0f0;
}
#result {
margin-top: 20px;
font-size: 24px;
}
</style>
</head>
<body>
<div class="container">
<h1>The Ultimate Answer</h1>
<p>Click the button to reveal the answer to life, the universe, and everything.</p>
<button id="showButton">Click me</button>
<div id="result"></div>
</div>
<script>
document.getElementById('showButton').addEventListener('click', function() {
document.getElementById('result').innerText = 'The answer is OpenHands is all you need!';
});
</script>
</body>
</html>
"""
class Test(BaseIntegrationTest):
INSTRUCTION = 'Browse localhost:8000, and tell me the ultimate answer to life.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
action = CmdRunAction(command='mkdir -p /tmp/server', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create a file with a typo in /workspace/bad.txt
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, 'index.html')
with open(temp_file_path, 'w') as f:
f.write(HTML_FILE)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/tmp/server')
# create README.md
action = CmdRunAction(
command='cd /tmp/server && nohup python3 -m http.server 8000 &',
keep_prompt=False,
)
obs = runtime.run_action(action)
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the "The answer is OpenHands is all you need!" is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
for event in message_actions:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
elif isinstance(event, MessageAction):
content = event.content
else:
raise ValueError(f'Unknown event type: {type(event)}')
if 'OpenHands is all you need!' in content:
return TestResult(success=True)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}. Messages: {message_actions}',
)
+4 -1
View File
@@ -477,9 +477,12 @@ if __name__ == '__main__':
if hasattr(_agent_cls, 'in_context_example'):
details['in_context_example'] = _agent_cls.in_context_example
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
'swe-bench-lite',
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
-1
View File
@@ -3,4 +3,3 @@ VITE_MOCK_API="false" # true or false
# GitHub OAuth
VITE_GITHUB_CLIENT_ID=""
VITE_APP_MODE="oss" # "oss" or "saas"
@@ -1,43 +0,0 @@
import { createRemixStub } from "@remix-run/testing";
import { describe, expect, it } from "vitest";
import { screen, within } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import userEvent from "@testing-library/user-event";
import CodeEditor from "#/routes/app._index/route";
const RemixStub = createRemixStub([{ path: "/app", Component: CodeEditor }]);
describe.skip("CodeEditor", () => {
it("should render", async () => {
renderWithProviders(<RemixStub initialEntries={["/app"]} />);
await screen.findByTestId("file-explorer");
expect(screen.getByTestId("code-editor-empty-message")).toBeInTheDocument();
});
it("should retrieve the files", async () => {
renderWithProviders(<RemixStub initialEntries={["/app"]} />);
const explorer = await screen.findByTestId("file-explorer");
const files = within(explorer).getAllByTestId("tree-node");
// request mocked with msw
expect(files).toHaveLength(3);
});
it("should open a file", async () => {
const user = userEvent.setup();
renderWithProviders(<RemixStub initialEntries={["/app"]} />);
const explorer = await screen.findByTestId("file-explorer");
const files = within(explorer).getAllByTestId("tree-node");
await user.click(files[0]);
// check if the file is opened
expect(
screen.queryByTestId("code-editor-empty-message"),
).not.toBeInTheDocument();
const editor = await screen.findByTestId("code-editor");
expect(
within(editor).getByText(/content of file1.ts/i),
).toBeInTheDocument();
});
});
-56
View File
@@ -1,56 +0,0 @@
import { createRemixStub } from "@remix-run/testing";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { ws } from "msw";
import { setupServer } from "msw/node";
import App from "#/routes/app";
import AgentState from "#/types/AgentState";
import { AgentStateChangeObservation } from "#/types/core/observations";
const RemixStub = createRemixStub([{ path: "/app", Component: App }]);
describe.skip("App", () => {
const agent = ws.link("ws://localhost:3001/ws");
const server = setupServer();
beforeAll(() => {
// mock `dom.scrollTo`
HTMLElement.prototype.scrollTo = vi.fn().mockImplementation(() => {});
});
it("should render", async () => {
render(<RemixStub initialEntries={["/app"]} />);
await waitFor(() => {
expect(screen.getByTestId("app")).toBeInTheDocument();
expect(
screen.getByText(/INITIALIZING_AGENT_LOADING_MESSAGE/i),
).toBeInTheDocument();
});
});
it("should establish a ws connection and send the init message", async () => {
server.use(
agent.addEventListener("connection", ({ client }) => {
client.send(
JSON.stringify({
id: 1,
cause: 0,
message: "AGENT_INIT_MESSAGE",
source: "agent",
timestamp: new Date().toISOString(),
observation: "agent_state_changed",
content: "AGENT_INIT_MESSAGE",
extras: { agent_state: AgentState.INIT },
} satisfies AgentStateChangeObservation),
);
}),
);
render(<RemixStub initialEntries={["/app"]} />);
await waitFor(() => {
expect(screen.getByText(/AGENT_INIT_MESSAGE/i)).toBeInTheDocument();
});
});
});
-50
View File
@@ -1,50 +0,0 @@
import { createRemixStub } from "@remix-run/testing";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Home from "#/routes/_index/route";
const renderRemixStub = (config?: { authenticated: boolean }) =>
createRemixStub([
{
path: "/",
Component: Home,
loader: () => ({
ghToken: config?.authenticated ? "ghp_123456" : null,
}),
},
]);
describe.skip("Home (_index)", () => {
it("should render", async () => {
const RemixStub = renderRemixStub();
render(<RemixStub />);
await screen.findByText(/let's start building/i);
});
it("should load the gh repos if a token is present", async () => {
const user = userEvent.setup();
const RemixStub = renderRemixStub({ authenticated: true });
render(<RemixStub />);
const repos = await screen.findByPlaceholderText(
/select a github project/i,
);
await user.click(repos);
// mocked responses from msw
screen.getByText(/octocat\/hello-world/i);
screen.getByText(/octocat\/earth/i);
});
it("should not load the gh repos if a token is not present", async () => {
const RemixStub = renderRemixStub();
render(<RemixStub />);
const repos = await screen.findByPlaceholderText(
/select a github project/i,
);
await userEvent.click(repos);
expect(screen.queryByText(/octocat\/hello-world/i)).not.toBeInTheDocument();
expect(screen.queryByText(/octocat\/earth/i)).not.toBeInTheDocument();
});
});
-40
View File
@@ -1,40 +0,0 @@
import { describe, expect, it } from "vitest";
import { createRemixStub } from "@remix-run/testing";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App, { clientLoader } from "#/root";
const RemixStub = createRemixStub([
{
path: "/",
Component: App,
loader: clientLoader,
},
]);
describe.skip("Root", () => {
it("should render", async () => {
render(<RemixStub />);
await screen.findByTestId("link-to-main");
});
describe("Auth Modal", () => {
it("should display the auth modal on first time visit", async () => {
render(<RemixStub />);
await screen.findByTestId("auth-modal");
});
it("should close the auth modal on accepting the terms", async () => {
const user = userEvent.setup();
render(<RemixStub />);
await screen.findByTestId("auth-modal");
await user.click(screen.getByTestId("accept-terms"));
await user.click(screen.getByRole("button", { name: /continue/i }));
expect(screen.queryByTestId("auth-modal")).not.toBeInTheDocument();
expect(screen.getByTestId("link-to-main")).toBeInTheDocument();
});
it.todo("should not display the auth modal on subsequent visits");
});
});
+3
View File
@@ -0,0 +1,3 @@
interface Window {
__APP_MODE__?: "saas" | "oss";
}
+81 -71
View File
@@ -35,7 +35,7 @@
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"tailwind-merge": "^2.5.4",
@@ -47,10 +47,10 @@
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.7.5",
"@types/node": "^22.7.6",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
@@ -1601,9 +1601,9 @@
}
},
"node_modules/@jspm/core": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz",
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.1.0.tgz",
"integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==",
"dev": true
},
"node_modules/@mdx-js/mdx": {
@@ -3560,15 +3560,15 @@
}
},
"node_modules/@react-aria/grid": {
"version": "3.10.4",
"resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.10.4.tgz",
"integrity": "sha512-3AjJ0hwRhOCIHThIZrGWrjAuKDpaZuBkODW3dvgLqtsNm3tL46DI6U9O3vfp8lNbrWMsXJgjRXwvXvdv0/gwCA==",
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.10.5.tgz",
"integrity": "sha512-9sLa+rpLgRZk7VX+tvdSudn1tdVgolVzhDLGWd95yS4UtPVMihTMGBrRoByY57Wxvh1V+7Ptw8kc6tsRSotYKg==",
"dependencies": {
"@react-aria/focus": "^3.18.3",
"@react-aria/focus": "^3.18.4",
"@react-aria/i18n": "^3.12.3",
"@react-aria/interactions": "^3.22.3",
"@react-aria/interactions": "^3.22.4",
"@react-aria/live-announcer": "^3.4.0",
"@react-aria/selection": "^3.20.0",
"@react-aria/selection": "^3.20.1",
"@react-aria/utils": "^3.25.3",
"@react-stately/collections": "^3.11.0",
"@react-stately/grid": "^3.9.3",
@@ -3584,11 +3584,11 @@
}
},
"node_modules/@react-aria/grid/node_modules/@react-aria/focus": {
"version": "3.18.3",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.3.tgz",
"integrity": "sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==",
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
"integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
"dependencies": {
"@react-aria/interactions": "^3.22.3",
"@react-aria/interactions": "^3.22.4",
"@react-aria/utils": "^3.25.3",
"@react-types/shared": "^3.25.0",
"@swc/helpers": "^0.5.0",
@@ -3617,9 +3617,9 @@
}
},
"node_modules/@react-aria/grid/node_modules/@react-aria/interactions": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.3.tgz",
"integrity": "sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==",
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
"integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
"dependencies": {
"@react-aria/ssr": "^3.9.6",
"@react-aria/utils": "^3.25.3",
@@ -3631,13 +3631,13 @@
}
},
"node_modules/@react-aria/grid/node_modules/@react-aria/selection": {
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.20.0.tgz",
"integrity": "sha512-h3giMcXo4SMZRL5HrqZvOLNTsdh5jCXwLUx0wpj/2EF0tcYQL6WDfn1iJ+rHARkUIs7X70fUV8iwlbUySZy1xg==",
"version": "3.20.1",
"resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.20.1.tgz",
"integrity": "sha512-My0w8UC/7PAkz/1yZUjr2VRuzDZz1RrbgTqP36j5hsJx8RczDTjI4TmKtQNKG0ggaP4w83G2Og5JPTq3w3LMAw==",
"dependencies": {
"@react-aria/focus": "^3.18.3",
"@react-aria/focus": "^3.18.4",
"@react-aria/i18n": "^3.12.3",
"@react-aria/interactions": "^3.22.3",
"@react-aria/interactions": "^3.22.4",
"@react-aria/utils": "^3.25.3",
"@react-stately/selection": "^3.17.0",
"@react-types/shared": "^3.25.0",
@@ -4110,12 +4110,12 @@
}
},
"node_modules/@react-aria/toggle": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.8.tgz",
"integrity": "sha512-N6WTgE8ByMYY+ZygUUPGON2vW5NrxwU91H98+Nozl+Rq6ZYR2fD9i8oRtLtrYPxjU2HmaFwDyQdWvmMJZuDxig==",
"version": "3.10.9",
"resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.9.tgz",
"integrity": "sha512-dtfnyIU2/kcH9rFAiB48diSmaXDv45K7UCuTkMQLjbQa3QHC1oYNbleVN/VdGyAMBsIWtfl8L4uuPrAQmDV/bg==",
"dependencies": {
"@react-aria/focus": "^3.18.3",
"@react-aria/interactions": "^3.22.3",
"@react-aria/focus": "^3.18.4",
"@react-aria/interactions": "^3.22.4",
"@react-aria/utils": "^3.25.3",
"@react-stately/toggle": "^3.7.8",
"@react-types/checkbox": "^3.8.4",
@@ -4127,11 +4127,11 @@
}
},
"node_modules/@react-aria/toggle/node_modules/@react-aria/focus": {
"version": "3.18.3",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.3.tgz",
"integrity": "sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==",
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
"integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
"dependencies": {
"@react-aria/interactions": "^3.22.3",
"@react-aria/interactions": "^3.22.4",
"@react-aria/utils": "^3.25.3",
"@react-types/shared": "^3.25.0",
"@swc/helpers": "^0.5.0",
@@ -4142,9 +4142,9 @@
}
},
"node_modules/@react-aria/toggle/node_modules/@react-aria/interactions": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.3.tgz",
"integrity": "sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==",
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
"integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
"dependencies": {
"@react-aria/ssr": "^3.9.6",
"@react-aria/utils": "^3.25.3",
@@ -5814,9 +5814,9 @@
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
"integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.1.tgz",
"integrity": "sha512-mNYIiAuP4yJwV2zBRQCV7PHoQwbb6/8TfMpPcwSUzcSVDJHWOXt6hjNtIN1v5knDmimYnjJxKhsoVd4LVGIO+w==",
"dev": true,
"dependencies": {
"@adobe/css-tools": "^4.4.0",
@@ -6028,9 +6028,9 @@
}
},
"node_modules/@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"version": "22.7.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz",
"integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==",
"devOptional": true,
"dependencies": {
"undici-types": "~6.19.2"
@@ -6619,9 +6619,9 @@
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -7325,9 +7325,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"version": "1.0.30001669",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
"integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
"funding": [
{
"type": "opencollective",
@@ -8396,9 +8396,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw=="
"version": "1.5.39",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz",
"integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -9840,9 +9840,9 @@
}
},
"node_modules/framer-motion": {
"version": "11.11.8",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.8.tgz",
"integrity": "sha512-mnGQNEoz99GtFXBBPw+Ag5K4FcfP5XrXxrxHz+iE4Lmg7W3sf2gKmGuvfkZCW/yIfcdv5vJd6KiSPETH1Pw68Q==",
"version": "11.11.9",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.9.tgz",
"integrity": "sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==",
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -10321,9 +10321,9 @@
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.1.tgz",
"integrity": "sha512-Rbemi1rzrkysSin0FDHZfsxYPoqLGHFfxFm28aOBHPibT7aqjy7kUgY636se9xbuCWUsFpWAYlmtGHQakiqtEA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
"integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==",
"dependencies": {
"@types/estree": "^1.0.0",
"@types/hast": "^3.0.0",
@@ -11043,6 +11043,11 @@
"node": "*"
}
},
"node_modules/highlightjs-vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="
},
"node_modules/hosted-git-info": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz",
@@ -20734,12 +20739,13 @@
}
},
"node_modules/react-syntax-highlighter": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
"integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
"prismjs": "^1.27.0",
"refractor": "^3.6.0"
@@ -22713,13 +22719,17 @@
}
},
"node_modules/string.prototype.includes": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz",
"integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
"integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
"dev": true,
"dependencies": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/string.prototype.matchall": {
@@ -23499,9 +23509,9 @@
}
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
},
"node_modules/turbo-stream": {
"version": "2.4.0",
@@ -23661,9 +23671,9 @@
}
},
"node_modules/undici": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.20.0.tgz",
"integrity": "sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==",
"version": "6.20.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz",
"integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==",
"engines": {
"node": ">=18.17"
}
+4 -4
View File
@@ -34,7 +34,7 @@
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"tailwind-merge": "^2.5.4",
@@ -46,7 +46,7 @@
"dev": "npm run make-i18n && VITE_MOCK_API=false remix vite:dev",
"dev:mock": "npm run make-i18n && VITE_MOCK_API=true remix vite:dev",
"build": "npm run make-i18n && tsc && remix vite:build",
"start": "npx sirv-cli build/client/ --single",
"start": "npx sirv-cli build/ --single",
"test": "vitest run",
"test:coverage": "npm run make-i18n && vitest run --coverage",
"dev_wsl": "VITE_WATCH_USE_POLLING=true vite",
@@ -72,10 +72,10 @@
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.7.5",
"@types/node": "^22.7.6",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
+3
View File
@@ -0,0 +1,3 @@
{
"APP_MODE": "oss"
}
+33 -18
View File
@@ -60,6 +60,15 @@ class OpenHands {
return response.json();
}
static async getConfig(): Promise<{ APP_MODE: "saas" | "oss" }> {
const response = await fetch(`${OpenHands.BASE_URL}/config.json`, {
headers: {
"Cache-Control": "no-cache",
},
});
return response.json();
}
/**
* Retrieve the list of files available in the workspace
* @param token User token provided by the server
@@ -71,7 +80,9 @@ class OpenHands {
if (path) url.searchParams.append("path", path);
const response = await fetch(url.toString(), {
headers: OpenHands.generateHeaders(token),
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.json();
@@ -87,7 +98,9 @@ class OpenHands {
const url = new URL(`${OpenHands.BASE_URL}/api/select-file`);
url.searchParams.append("file", path);
const response = await fetch(url.toString(), {
headers: OpenHands.generateHeaders(token),
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
@@ -109,7 +122,10 @@ class OpenHands {
const response = await fetch(`${OpenHands.BASE_URL}/api/save-file`, {
method: "POST",
body: JSON.stringify({ filePath: path, content }),
headers: OpenHands.generateHeaders(token),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
return response.json();
@@ -130,8 +146,10 @@ class OpenHands {
const response = await fetch(`${OpenHands.BASE_URL}/api/upload-files`, {
method: "POST",
headers: OpenHands.generateHeaders(token),
body: formData,
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.json();
@@ -144,8 +162,11 @@ class OpenHands {
*/
static async getWorkspaceZip(token: string): Promise<Blob> {
const response = await fetch(`${OpenHands.BASE_URL}/api/zip-directory`, {
headers: OpenHands.generateHeaders(token),
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.blob();
}
@@ -158,12 +179,14 @@ class OpenHands {
static async sendFeedback(
token: string,
data: Feedback,
// TODO: Type the response
): Promise<FeedbackResponse> {
const response = await fetch(`${OpenHands.BASE_URL}/api/submit-feedback`, {
method: "POST",
headers: OpenHands.generateHeaders(token),
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
return response.json();
@@ -180,21 +203,13 @@ class OpenHands {
const response = await fetch(`${OpenHands.BASE_URL}/github/callback`, {
method: "POST",
body: JSON.stringify({ code }),
headers: {
"Content-Type": "application/json",
},
});
return response.json();
}
/**
* Generate the headers for the request
* @param token User token provided by the server
* @returns Headers for the request
*/
private static generateHeaders(token: string) {
return {
Authorization: `Bearer ${token}`,
};
}
}
export default OpenHands;
+4 -4
View File
@@ -4,8 +4,8 @@ import React from "react";
import AgentControlBar from "./AgentControlBar";
import AgentStatusBar from "./AgentStatusBar";
import { ProjectMenuCard } from "./project-menu/ProjectMenuCard";
import { clientLoader as rootClientLoader } from "#/root";
import { clientLoader as appClientLoader } from "#/routes/app";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { clientLoader as appClientLoader } from "#/routes/_oh.app";
import { isGitHubErrorReponse } from "#/api/github";
interface ControlsProps {
@@ -19,8 +19,8 @@ export function Controls({
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const rootData = useRouteLoaderData<typeof rootClientLoader>("root");
const appData = useRouteLoaderData<typeof appClientLoader>("routes/app");
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const appData = useRouteLoaderData<typeof appClientLoader>("routes/_oh.app");
const projectMenuCardData = React.useMemo(
() =>
@@ -0,0 +1,62 @@
import { cn } from "@nextui-org/react";
import { HTMLAttributes } from "react";
interface EditorActionButtonProps {
onClick: () => void;
disabled: boolean;
className: HTMLAttributes<HTMLButtonElement>["className"];
}
function EditorActionButton({
onClick,
disabled,
className,
children,
}: React.PropsWithChildren<EditorActionButtonProps>) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={cn(
"text-sm py-0.5 rounded w-20",
"hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
>
{children}
</button>
);
}
interface EditorActionsProps {
onSave: () => void;
onDiscard: () => void;
isDisabled: boolean;
}
export function EditorActions({
onSave,
onDiscard,
isDisabled,
}: EditorActionsProps) {
return (
<div className="flex gap-2">
<EditorActionButton
onClick={onSave}
disabled={isDisabled}
className="bg-neutral-800 disabled:hover:bg-neutral-800"
>
Save
</EditorActionButton>
<EditorActionButton
onClick={onDiscard}
disabled={isDisabled}
className="border border-neutral-800 disabled:hover:bg-transparent"
>
Discard
</EditorActionButton>
</div>
);
}
@@ -5,7 +5,7 @@ import ModalBody from "./ModalBody";
import ModalButton from "../buttons/ModalButton";
import FormFieldset from "../form/FormFieldset";
import { CustomInput } from "../form/custom-input";
import { clientLoader } from "#/root";
import { clientLoader } from "#/routes/_oh";
import { clientAction as settingsClientAction } from "#/routes/settings";
import { clientAction as loginClientAction } from "#/routes/login";
import { AvailableLanguages } from "#/i18n";
@@ -21,7 +21,7 @@ function AccountSettingsModal({
selectedLanguage,
gitHubError,
}: AccountSettingsModalProps) {
const data = useRouteLoaderData<typeof clientLoader>("root");
const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
const settingsFetcher = useFetcher<typeof settingsClientAction>({
key: "settings",
});
@@ -1,5 +1,4 @@
import { useFetcher, useRouteLoaderData } from "@remix-run/react";
import React from "react";
import ModalBody from "./ModalBody";
import { CustomInput } from "../form/custom-input";
import ModalButton from "../buttons/ModalButton";
@@ -7,7 +6,7 @@ import {
BaseModalDescription,
BaseModalTitle,
} from "./confirmation-modals/BaseModal";
import { clientLoader } from "#/root";
import { clientLoader } from "#/routes/_oh";
import { clientAction } from "#/routes/login";
interface ConnectToGitHubModalProps {
@@ -15,7 +14,7 @@ interface ConnectToGitHubModalProps {
}
export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
const data = useRouteLoaderData<typeof clientLoader>("root");
const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
const fetcher = useFetcher<typeof clientAction>({ key: "login" });
return (
@@ -1,51 +0,0 @@
import { useFetcher } from "@remix-run/react";
import ModalButton from "../buttons/ModalButton";
import { BaseModalTitle } from "./confirmation-modals/BaseModal";
import ModalBody from "./ModalBody";
import { CustomInput } from "../form/custom-input";
import { clientAction } from "#/routes/create-repository";
import { isGitHubErrorReponse } from "#/api/github";
interface PushToGitHubModalProps {
token: string;
onClose: () => void;
}
export function PushToGitHubModal({ token, onClose }: PushToGitHubModalProps) {
const fetcher = useFetcher<typeof clientAction>();
const actionData = fetcher.data;
return (
<ModalBody>
<BaseModalTitle title="Push to GitHub" />
<fetcher.Form
method="POST"
action="/create-repository"
className="w-full flex flex-col gap-6"
>
{actionData && isGitHubErrorReponse(actionData) && (
<div className="text-red-500 text-sm">{actionData.message}</div>
)}
<input type="text" hidden name="ghToken" defaultValue={token} />
<CustomInput name="repositoryName" label="Repository Name" required />
<CustomInput
name="repositoryDescription"
label="Repository Description"
/>
<div className="w-full flex flex-col gap-2">
<ModalButton
type="submit"
text="Create"
disabled={fetcher.state === "submitting"}
className="bg-[#4465DB] w-full"
/>
<ModalButton
text="Close"
className="bg-[#737373] w-full"
onClick={onClose}
/>
</div>
</fetcher.Form>
</ModalBody>
);
}
+13 -4
View File
@@ -27,6 +27,7 @@ interface FilesContextType {
modifiedFiles: Record<string, string>;
modifyFileContent: (path: string, content: string) => void;
saveFileContent: (path: string) => string | undefined;
discardChanges: (path: string) => void;
}
const FilesContext = React.createContext<FilesContextType | undefined>(
@@ -62,19 +63,25 @@ function FilesProvider({ children }: FilesProviderProps) {
[files, modifiedFiles],
);
const discardChanges = React.useCallback((path: string) => {
setModifiedFiles((prev) => {
const newModifiedFiles = { ...prev };
delete newModifiedFiles[path];
return newModifiedFiles;
});
}, []);
const saveFileContent = React.useCallback(
(path: string): string | undefined => {
const content = modifiedFiles[path];
if (content) {
setFiles((prev) => ({ ...prev, [path]: content }));
const newModifiedFiles = { ...modifiedFiles };
delete newModifiedFiles[path];
setModifiedFiles(newModifiedFiles);
discardChanges(path);
}
return content;
},
[files, modifiedFiles, selectedPath],
[files, modifiedFiles, selectedPath, discardChanges],
);
const value = React.useMemo(
@@ -88,6 +95,7 @@ function FilesProvider({ children }: FilesProviderProps) {
modifiedFiles,
modifyFileContent,
saveFileContent,
discardChanges,
}),
[
paths,
@@ -99,6 +107,7 @@ function FilesProvider({ children }: FilesProviderProps) {
modifiedFiles,
modifyFileContent,
saveFileContent,
discardChanges,
],
);
+1 -243
View File
@@ -5,31 +5,11 @@ import {
Outlet,
Scripts,
ScrollRestoration,
defer,
useFetcher,
useLoaderData,
useLocation,
useNavigation,
} from "@remix-run/react";
import "./tailwind.css";
import "./index.css";
import React from "react";
import { Toaster } from "react-hot-toast";
import CogTooth from "./assets/cog-tooth";
import { SettingsForm } from "./components/form/settings-form";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { isGitHubErrorReponse, retrieveGitHubUser } from "./api/github";
import OpenHands from "./api/open-hands";
import LoadingProjectModal from "./components/modals/LoadingProject";
import { getSettings, settingsAreUpToDate } from "./services/settings";
import AccountSettingsModal from "./components/modals/AccountSettingsModal";
import NewProjectIcon from "./assets/new-project.svg?react";
import DocsIcon from "./assets/docs.svg?react";
import i18n from "./i18n";
import { useSocket } from "./context/socket";
import { UserAvatar } from "./components/user-avatar";
import { DangerModal } from "./components/modals/confirmation-modals/danger-modal";
export function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -55,228 +35,6 @@ export const meta: MetaFunction = () => [
{ name: "description", content: "Let's Start Building!" },
];
export const clientLoader = async () => {
let token = localStorage.getItem("token");
const ghToken = localStorage.getItem("ghToken");
let user: GitHubUser | GitHubErrorReponse | null = null;
if (ghToken) user = await retrieveGitHubUser(ghToken);
const settings = getSettings();
await i18n.changeLanguage(settings.LANGUAGE);
const settingsIsUpdated = settingsAreUpToDate();
if (!settingsIsUpdated) {
localStorage.removeItem("token");
token = null;
}
return defer({
token,
ghToken,
user,
settingsIsUpdated,
settings,
});
};
export default function App() {
const { stop, isConnected } = useSocket();
const navigation = useNavigation();
const location = useLocation();
const { token, user, settingsIsUpdated, settings } =
useLoaderData<typeof clientLoader>();
const loginFetcher = useFetcher({ key: "login" });
const logoutFetcher = useFetcher({ key: "logout" });
const endSessionFetcher = useFetcher({ key: "end-session" });
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
React.useState(false);
const [data, setData] = React.useState<{
models: string[];
agents: string[];
securityAnalyzers: string[];
}>({
models: [],
agents: [],
securityAnalyzers: [],
});
React.useEffect(() => {
// We fetch this here instead of the data loader because the server seems to block
// the retrieval when the session is closing -- preventing the screen from rendering until
// the fetch is complete
(async () => {
const [models, agents, securityAnalyzers] = await Promise.all([
OpenHands.getModels(),
OpenHands.getAgents(),
OpenHands.getSecurityAnalyzers(),
]);
setData({ models, agents, securityAnalyzers });
})();
}, []);
React.useEffect(() => {
// If the github token is invalid, open the account settings modal again
if (isGitHubErrorReponse(user)) {
setAccountSettingsModalOpen(true);
}
}, [user]);
React.useEffect(() => {
if (location.pathname === "/") {
// If the user is on the home page, we should stop the socket connection.
// This is relevant when the user redirects here for whatever reason.
if (isConnected) stop();
}
}, [location.pathname]);
const handleUserLogout = () => {
logoutFetcher.submit(
{},
{
method: "POST",
action: "/logout",
},
);
};
const handleAccountSettingsModalClose = () => {
// If the user closes the modal without connecting to GitHub,
// we need to log them out to clear the invalid token from the
// local storage
if (isGitHubErrorReponse(user)) handleUserLogout();
setAccountSettingsModalOpen(false);
};
const handleEndSession = () => {
setStartNewProjectModalIsOpen(false);
// call new session action and redirect to '/'
endSessionFetcher.submit(new FormData(), {
method: "POST",
action: "/end-session",
});
};
return (
<div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
<aside className="px-1 flex flex-col gap-[15px]">
<button
type="button"
aria-label="All Hands Logo"
onClick={() => {
if (location.pathname !== "/") setStartNewProjectModalIsOpen(true);
}}
>
<AllHandsLogo width={34} height={23} />
</button>
<nav className="py-[18px] flex flex-col items-center gap-[18px]">
<UserAvatar
user={user}
isLoading={loginFetcher.state !== "idle"}
onLogout={handleUserLogout}
handleOpenAccountSettingsModal={() =>
setAccountSettingsModalOpen(true)
}
/>
<button
type="button"
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
onClick={() => setSettingsModalIsOpen(true)}
aria-label="Settings"
>
<CogTooth />
</button>
<a
href="https://docs.all-hands.dev"
target="_blank"
rel="noreferrer noopener"
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
aria-label="Documentation"
>
<DocsIcon width={28} height={28} />
</a>
{!!token && (
<button
type="button"
aria-label="Start new project"
onClick={() => setStartNewProjectModalIsOpen(true)}
>
<NewProjectIcon width={28} height={28} />
</button>
)}
</nav>
</aside>
<div className="h-full w-full relative">
<Outlet />
{navigation.state === "loading" && location.pathname !== "/" && (
<ModalBackdrop>
<LoadingProjectModal
message={
endSessionFetcher.state === "loading"
? "Ending session, please wait..."
: undefined
}
/>
</ModalBackdrop>
)}
{(!settingsIsUpdated || settingsModalIsOpen) && (
<ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
<div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
<span className="text-xl leading-6 font-semibold -tracking-[0.01em">
AI Provider Configuration
</span>
<p className="text-xs text-[#A3A3A3]">
To continue, connect an OpenAI, Anthropic, or other LLM account
</p>
{isConnected && (
<p className="text-xs text-danger">
Changing settings during an active session will end the
session
</p>
)}
<SettingsForm
settings={settings}
models={data.models}
agents={data.agents}
securityAnalyzers={data.securityAnalyzers}
onClose={() => setSettingsModalIsOpen(false)}
/>
</div>
</ModalBackdrop>
)}
{accountSettingsModalOpen && (
<ModalBackdrop onClose={handleAccountSettingsModalClose}>
<AccountSettingsModal
onClose={handleAccountSettingsModalClose}
selectedLanguage={settings.LANGUAGE}
gitHubError={isGitHubErrorReponse(user)}
/>
</ModalBackdrop>
)}
{startNewProjectModalIsOpen && (
<ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
<DangerModal
title="Are you sure you want to exit?"
description="You will lose any unsaved information."
buttons={{
danger: {
text: "Exit Project",
onClick: handleEndSession,
},
cancel: {
text: "Cancel",
onClick: () => setStartNewProjectModalIsOpen(false),
},
}}
/>
</ModalBackdrop>
)}
</div>
</div>
);
return <Outlet />;
}
@@ -24,7 +24,7 @@ import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { LoadingSpinner } from "#/components/modals/LoadingProject";
import store, { RootState } from "#/store";
import { removeFile, setInitialQuery } from "#/state/initial-query-slice";
import { clientLoader as rootClientLoader } from "#/root";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { UploadedFilePreview } from "./uploaded-file-preview";
interface AttachedFilesSliderProps {
@@ -87,7 +87,7 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID;
const requestUrl = new URL(request.url);
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,user`;
const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,user,workflow`;
return json({ repositories, githubAuthUrl });
};
@@ -101,7 +101,7 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
};
function Home() {
const rootData = useRouteLoaderData<typeof rootClientLoader>("root");
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const navigation = useNavigation();
const { repositories, githubAuthUrl } = useLoaderData<typeof clientLoader>();
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
@@ -113,7 +113,7 @@ function Home() {
const { files } = useSelector((state: RootState) => state.initalQuery);
const handleConnectToGitHub = () => {
const isSaas = import.meta.env.VITE_APP_MODE === "saas";
const isSaas = window.__APP_MODE__ === "saas";
if (isSaas) {
window.location.href = githubAuthUrl;
@@ -13,6 +13,7 @@ import OpenHands from "#/api/open-hands";
import { useSocket } from "#/context/socket";
import CodeEditorCompoonent from "./code-editor-component";
import { useFiles } from "#/context/files";
import { EditorActions } from "#/components/editor-actions";
export const clientLoader = async () => {
const token = localStorage.getItem("token");
@@ -48,7 +49,13 @@ export function ErrorBoundary() {
function CodeEditor() {
const { token } = useLoaderData<typeof clientLoader>();
const { runtimeActive } = useSocket();
const { setPaths } = useFiles();
const {
setPaths,
selectedPath,
modifiedFiles,
saveFileContent: saveNewFileContent,
discardChanges,
} = useFiles();
const agentState = useSelector(
(state: RootState) => state.agent.curAgentState,
@@ -68,10 +75,38 @@ function CodeEditor() {
[agentState],
);
const handleSave = async () => {
if (selectedPath) {
const content = saveNewFileContent(selectedPath);
if (content && token) {
try {
await OpenHands.saveFile(token, selectedPath, content);
} catch (error) {
// handle error
}
}
}
};
const handleDiscard = () => {
if (selectedPath) discardChanges(selectedPath);
};
return (
<div className="flex h-full w-full bg-neutral-900 relative">
<FileExplorer />
<div className="flex flex-col min-h-0 w-full pt-3">
<div className="flex flex-col min-h-0 w-full">
{selectedPath && (
<div className="flex w-full items-center justify-between self-end p-2">
<span className="text-sm text-neutral-500">{selectedPath}</span>
<EditorActions
onSave={handleSave}
onDiscard={handleDiscard}
isDisabled={!isEditingAllowed || !modifiedFiles[selectedPath]}
/>
</div>
)}
<div className="flex grow items-center justify-center">
<CodeEditorCompoonent isReadOnly={!isEditingAllowed} />
</div>
@@ -39,7 +39,7 @@ import { isGitHubErrorReponse, retrieveLatestGitHubCommit } from "#/api/github";
import OpenHands from "#/api/open-hands";
import AgentState from "#/types/AgentState";
import { base64ToBlob } from "#/utils/base64-to-blob";
import { clientLoader as rootClientLoader } from "#/root";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { clearJupyter } from "#/state/jupyterSlice";
import { FilesProvider } from "#/context/files";
@@ -111,7 +111,7 @@ function App() {
const { settings, token, ghToken, repo, q, lastCommit } =
useLoaderData<typeof clientLoader>();
const fetcher = useFetcher();
const data = useRouteLoaderData<typeof rootClientLoader>("root");
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
// To avoid re-rendering the component when the user object changes, we memoize the user ID.
// We use this to ensure the github token is valid before exporting it to the terminal.
+291
View File
@@ -0,0 +1,291 @@
import React from "react";
import {
defer,
useRouteError,
isRouteErrorResponse,
useNavigation,
useLocation,
useLoaderData,
useFetcher,
Outlet,
} from "@remix-run/react";
import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github";
import OpenHands from "#/api/open-hands";
import CogTooth from "#/assets/cog-tooth";
import { SettingsForm } from "#/components/form/settings-form";
import AccountSettingsModal from "#/components/modals/AccountSettingsModal";
import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal";
import LoadingProjectModal from "#/components/modals/LoadingProject";
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { UserAvatar } from "#/components/user-avatar";
import { useSocket } from "#/context/socket";
import i18n from "#/i18n";
import { getSettings, settingsAreUpToDate } from "#/services/settings";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import NewProjectIcon from "#/assets/new-project.svg?react";
import DocsIcon from "#/assets/docs.svg?react";
export const clientLoader = async () => {
try {
const config = await OpenHands.getConfig();
window.__APP_MODE__ = config.APP_MODE;
} catch (error) {
window.__APP_MODE__ = "oss";
}
let token = localStorage.getItem("token");
const ghToken = localStorage.getItem("ghToken");
let user: GitHubUser | GitHubErrorReponse | null = null;
if (ghToken) user = await retrieveGitHubUser(ghToken);
const settings = getSettings();
await i18n.changeLanguage(settings.LANGUAGE);
const settingsIsUpdated = settingsAreUpToDate();
if (!settingsIsUpdated) {
localStorage.removeItem("token");
token = null;
}
return defer({
token,
ghToken,
user,
settingsIsUpdated,
settings,
});
};
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.statusText}</p>
<pre>
{error.data instanceof Object
? JSON.stringify(error.data)
: error.data}
</pre>
</div>
);
}
if (error instanceof Error) {
return (
<div>
<h1>Uh oh, an error occurred!</h1>
<pre>{error.message}</pre>
</div>
);
}
return (
<div>
<h1>Uh oh, an unknown error occurred!</h1>
</div>
);
}
export default function MainApp() {
const { stop, isConnected } = useSocket();
const navigation = useNavigation();
const location = useLocation();
const { token, user, settingsIsUpdated, settings } =
useLoaderData<typeof clientLoader>();
const loginFetcher = useFetcher({ key: "login" });
const logoutFetcher = useFetcher({ key: "logout" });
const endSessionFetcher = useFetcher({ key: "end-session" });
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
React.useState(false);
const [data, setData] = React.useState<{
models: string[];
agents: string[];
securityAnalyzers: string[];
}>({
models: [],
agents: [],
securityAnalyzers: [],
});
React.useEffect(() => {
// We fetch this here instead of the data loader because the server seems to block
// the retrieval when the session is closing -- preventing the screen from rendering until
// the fetch is complete
(async () => {
const [models, agents, securityAnalyzers] = await Promise.all([
OpenHands.getModels(),
OpenHands.getAgents(),
OpenHands.getSecurityAnalyzers(),
]);
setData({ models, agents, securityAnalyzers });
})();
}, []);
React.useEffect(() => {
// If the github token is invalid, open the account settings modal again
if (isGitHubErrorReponse(user)) {
setAccountSettingsModalOpen(true);
}
}, [user]);
React.useEffect(() => {
if (location.pathname === "/") {
// If the user is on the home page, we should stop the socket connection.
// This is relevant when the user redirects here for whatever reason.
if (isConnected) stop();
}
}, [location.pathname]);
const handleUserLogout = () => {
logoutFetcher.submit(
{},
{
method: "POST",
action: "/logout",
},
);
};
const handleAccountSettingsModalClose = () => {
// If the user closes the modal without connecting to GitHub,
// we need to log them out to clear the invalid token from the
// local storage
if (isGitHubErrorReponse(user)) handleUserLogout();
setAccountSettingsModalOpen(false);
};
const handleEndSession = () => {
setStartNewProjectModalIsOpen(false);
// call new session action and redirect to '/'
endSessionFetcher.submit(new FormData(), {
method: "POST",
action: "/end-session",
});
};
return (
<div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
<aside className="px-1 flex flex-col gap-[15px]">
<button
type="button"
aria-label="All Hands Logo"
onClick={() => {
if (location.pathname !== "/") setStartNewProjectModalIsOpen(true);
}}
>
<AllHandsLogo width={34} height={23} />
</button>
<nav className="py-[18px] flex flex-col items-center gap-[18px]">
<UserAvatar
user={user}
isLoading={loginFetcher.state !== "idle"}
onLogout={handleUserLogout}
handleOpenAccountSettingsModal={() =>
setAccountSettingsModalOpen(true)
}
/>
<button
type="button"
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
onClick={() => setSettingsModalIsOpen(true)}
aria-label="Settings"
>
<CogTooth />
</button>
<a
href="https://docs.all-hands.dev"
target="_blank"
rel="noreferrer noopener"
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
aria-label="Documentation"
>
<DocsIcon width={28} height={28} />
</a>
{!!token && (
<button
type="button"
aria-label="Start new project"
onClick={() => setStartNewProjectModalIsOpen(true)}
>
<NewProjectIcon width={28} height={28} />
</button>
)}
</nav>
</aside>
<div className="h-full w-full relative">
<Outlet />
{navigation.state === "loading" && location.pathname !== "/" && (
<ModalBackdrop>
<LoadingProjectModal
message={
endSessionFetcher.state === "loading"
? "Ending session, please wait..."
: undefined
}
/>
</ModalBackdrop>
)}
{(!settingsIsUpdated || settingsModalIsOpen) && (
<ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
<div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
<span className="text-xl leading-6 font-semibold -tracking-[0.01em">
AI Provider Configuration
</span>
<p className="text-xs text-[#A3A3A3]">
To continue, connect an OpenAI, Anthropic, or other LLM account
</p>
{isConnected && (
<p className="text-xs text-danger">
Changing settings during an active session will end the
session
</p>
)}
<SettingsForm
settings={settings}
models={data.models}
agents={data.agents}
securityAnalyzers={data.securityAnalyzers}
onClose={() => setSettingsModalIsOpen(false)}
/>
</div>
</ModalBackdrop>
)}
{accountSettingsModalOpen && (
<ModalBackdrop onClose={handleAccountSettingsModalClose}>
<AccountSettingsModal
onClose={handleAccountSettingsModalClose}
selectedLanguage={settings.LANGUAGE}
gitHubError={isGitHubErrorReponse(user)}
/>
</ModalBackdrop>
)}
{startNewProjectModalIsOpen && (
<ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
<DangerModal
title="Are you sure you want to exit?"
description="You will lose any unsaved information."
buttons={{
danger: {
text: "Exit Project",
onClick: handleEndSession,
},
cancel: {
text: "Cancel",
onClick: () => setStartNewProjectModalIsOpen(false),
},
}}
/>
</ModalBackdrop>
)}
</div>
</div>
);
}
-23
View File
@@ -1,23 +0,0 @@
import { ClientActionFunctionArgs, json } from "@remix-run/react";
import { createGitHubRepository } from "#/api/github";
export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
const formData = await request.formData();
const token = formData.get("ghToken")?.toString();
const repositoryName = formData.get("repositoryName")?.toString();
const repositoryDescription = formData
.get("repositoryDescription")
?.toString();
if (token && repositoryName) {
const response = await createGitHubRepository(
token,
repositoryName,
repositoryDescription,
);
return json(response);
}
return json(null);
};
@@ -11,8 +11,7 @@
*/
export const getValidFallbackHost = () => {
if (typeof window !== "undefined") {
const { hostname, host } = window.location;
if (hostname !== "localhost") return host;
return window.location.host;
}
// Fallback is localhost:3000 because that is the default port for the server
+91 -28
View File
@@ -1,37 +1,100 @@
/* eslint-disable import/no-extraneous-dependencies */
/// <reference types="vitest" />
/// <reference types="vite-plugin-svgr/client" />
import { defineConfig } from "vite";
import { defineConfig, loadEnv } from "vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
import svgr from "vite-plugin-svgr";
import { vitePlugin as remix } from "@remix-run/dev";
export default defineConfig(() => ({
plugins: [
!process.env.VITEST &&
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
export default defineConfig(({ mode }) => {
const {
VITE_BACKEND_HOST = "127.0.0.1:3000",
VITE_USE_TLS = "false",
VITE_FRONTEND_PORT = "3001",
VITE_INSECURE_SKIP_VERIFY = "false",
} = loadEnv(mode, process.cwd());
const USE_TLS = VITE_USE_TLS === "true";
const INSECURE_SKIP_VERIFY = VITE_INSECURE_SKIP_VERIFY === "true";
const PROTOCOL = USE_TLS ? "https" : "http";
const WS_PROTOCOL = USE_TLS ? "wss" : "ws";
const API_URL = `${PROTOCOL}://${VITE_BACKEND_HOST}/`;
const WS_URL = `${WS_PROTOCOL}://${VITE_BACKEND_HOST}/`;
const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10);
/**
* This script is used to unpack the client directory from the frontend build directory.
* Remix SPA mode builds the client directory into the build directory. This function
* moves the contents of the client directory to the build directory and then removes the
* client directory.
*
* This script is used in the buildEnd function of the Vite config.
*/
const unpackClientDirectory = async () => {
const fs = await import("fs");
const path = await import("path");
const buildDir = path.resolve(__dirname, "build");
const clientDir = path.resolve(buildDir, "client");
const files = await fs.promises.readdir(clientDir);
await Promise.all(
files.map((file) =>
fs.promises.rename(
path.resolve(clientDir, file),
path.resolve(buildDir, file),
),
),
);
await fs.promises.rmdir(clientDir);
};
return {
plugins: [
!process.env.VITEST &&
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
},
appDirectory: "src",
buildEnd: unpackClientDirectory,
ssr: false,
}),
viteTsconfigPaths(),
svgr(),
],
server: {
port: FE_PORT,
proxy: {
"/api": {
target: API_URL,
changeOrigin: true,
secure: !INSECURE_SKIP_VERIFY,
},
appDirectory: "src",
ssr: false,
}),
viteTsconfigPaths(),
svgr(),
],
ssr: {
noExternal: ["react-syntax-highlighter"],
},
clearScreen: false,
test: {
environment: "jsdom",
setupFiles: ["vitest.setup.ts"],
coverage: {
reporter: ["text", "json", "html", "lcov", "text-summary"],
reportsDirectory: "coverage",
include: ["src/**/*.{ts,tsx}"],
"/ws": {
target: WS_URL,
ws: true,
changeOrigin: true,
secure: !INSECURE_SKIP_VERIFY,
},
},
},
},
}));
ssr: {
noExternal: ["react-syntax-highlighter"],
},
clearScreen: false,
test: {
environment: "jsdom",
setupFiles: ["vitest.setup.ts"],
coverage: {
reporter: ["text", "json", "html", "lcov", "text-summary"],
reportsDirectory: "coverage",
include: ["src/**/*.{ts,tsx}"],
},
},
};
});
+2 -1
View File
@@ -1,6 +1,7 @@
# Task
You are in charge of accomplishing the following task:
{{ latest_user_message }}
{% set goal = latest_user_message if latest_user_message is not none else state.inputs.task %}
{{ goal }}
In order to accomplish this goal, you must delegate tasks to one or more agents, who
can do the actual work. A description of each agent is provided below. You MUST
-3
View File
@@ -59,9 +59,6 @@ class DockerRuntimeBuilder(RuntimeBuilder):
target_image_repo, target_image_hash_tag = target_image_hash_name.split(':')
target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None
# Check if the image exists and pull if necessary
self.image_exists(target_image_hash_name)
buildx_cmd = [
'docker',
'buildx',
+6 -4
View File
@@ -2,6 +2,7 @@ import os
import tempfile
import threading
import uuid
from pathlib import Path
from typing import Callable, Generator
import modal
@@ -14,7 +15,7 @@ from openhands.events import EventStream
from openhands.runtime.client.runtime import EventStreamRuntime, LogBuffer
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.runtime_build import (
prep_docker_build_folder,
prep_build_folder,
)
@@ -148,9 +149,10 @@ class ModalRuntime(EventStreamRuntime):
base_runtime_image = modal.Image.from_registry(runtime_container_image_id)
elif base_container_image_id:
build_folder = tempfile.mkdtemp()
prep_docker_build_folder(
build_folder,
base_container_image_id,
prep_build_folder(
build_folder=Path(build_folder),
base_image=base_container_image_id,
build_from_scratch=True,
extra_deps=runtime_extra_deps,
)
+177 -247
View File
@@ -1,16 +1,17 @@
import argparse
import hashlib
import importlib.metadata
import os
import shutil
import string
import tempfile
from pathlib import Path
from typing import List
import docker
from dirhash import dirhash
from jinja2 import Environment, FileSystemLoader
import openhands
from openhands import __package_name__
from openhands import __version__ as oh_version
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.builder import DockerRuntimeBuilder, RuntimeBuilder
@@ -20,65 +21,16 @@ def get_runtime_image_repo():
return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime')
def _put_source_code_to_dir(temp_dir: str):
"""Builds the project source tarball directly in temp_dir and unpacks it.
The OpenHands source code ends up in the temp_dir/code directory.
Parameters:
- temp_dir (str): The directory to put the source code in
"""
if not os.path.isdir(temp_dir):
raise RuntimeError(f'Temp directory {temp_dir} does not exist')
dest_dir = os.path.join(temp_dir, 'code')
openhands_dir = None
try:
# Try to get the source directory from the installed package
distribution = importlib.metadata.distribution(__package_name__)
source_dir = os.path.dirname(distribution.locate_file(__package_name__))
openhands_dir = os.path.join(source_dir, 'openhands')
except importlib.metadata.PackageNotFoundError:
pass
if openhands_dir is not None and os.path.isdir(openhands_dir):
logger.info(f'Package {__package_name__} found')
shutil.copytree(openhands_dir, os.path.join(dest_dir, 'openhands'))
# note: "pyproject.toml" and "poetry.lock" are included in the openhands
# package, so we need to move them out to the top-level directory
for filename in ['pyproject.toml', 'poetry.lock']:
shutil.move(os.path.join(dest_dir, 'openhands', filename), dest_dir)
else:
# If package is not found, build from source code
project_root = os.path.dirname(
os.path.dirname(os.path.abspath(openhands.__file__))
)
logger.info(f'Building source distribution using project root: {project_root}')
# Copy the 'openhands' directory
openhands_dir = os.path.join(project_root, 'openhands')
if not os.path.isdir(openhands_dir):
raise RuntimeError(f"'openhands' directory not found in {project_root}")
shutil.copytree(openhands_dir, os.path.join(dest_dir, 'openhands'))
# Copy pyproject.toml and poetry.lock files
for file in ['pyproject.toml', 'poetry.lock']:
src_file = os.path.join(project_root, file)
dest_file = os.path.join(dest_dir, file)
shutil.copy2(src_file, dest_file)
logger.info(f'Unpacked source code directory: {dest_dir}')
def _generate_dockerfile(
base_image: str,
skip_init: bool = False,
build_from_scratch: bool = True,
extra_deps: str | None = None,
) -> str:
"""Generate the Dockerfile content for the runtime image based on the base image.
Parameters:
- base_image (str): The base image provided for the runtime image
- skip_init (boolean):
- build_from_scratch (boolean): False implies most steps can be skipped (Base image is another openhands instance)
- extra_deps (str):
Returns:
@@ -93,69 +45,12 @@ def _generate_dockerfile(
dockerfile_content = template.render(
base_image=base_image,
skip_init=skip_init,
build_from_scratch=build_from_scratch,
extra_deps=extra_deps if extra_deps is not None else '',
)
return dockerfile_content
def prep_docker_build_folder(
dir_path: str,
base_image: str,
skip_init: bool = False,
extra_deps: str | None = None,
) -> str:
"""Prepares a docker build folder by copying the source code and generating the Dockerfile
Parameters:
- dir_path (str): The build folder to place the source code and Dockerfile
- base_image (str): The base Docker image to use for the Dockerfile
- skip_init (str):
- extra_deps (str):
Returns:
- str: The MD5 hash of the build folder directory (dir_path)
"""
# Copy the source code to directory. It will end up in dir_path/code
_put_source_code_to_dir(dir_path)
# Create a Dockerfile and write it to dir_path
dockerfile_content = _generate_dockerfile(
base_image,
skip_init=skip_init,
extra_deps=extra_deps,
)
if os.getenv('SKIP_CONTAINER_LOGS', 'false') != 'true':
logger.debug(
(
f'===== Dockerfile content start =====\n'
f'{dockerfile_content}\n'
f'===== Dockerfile content end ====='
)
)
with open(os.path.join(dir_path, 'Dockerfile'), 'w') as file:
file.write(dockerfile_content)
# Get the MD5 hash of the dir_path directory
dir_hash = dirhash(
dir_path,
'md5',
ignore=[
'.*/', # hidden directories
'__pycache__/',
'*.pyc',
],
)
hash = f'v{oh_version}_{dir_hash}'
logger.info(
f'Input base image: {base_image}\n'
f'Skip init: {skip_init}\n'
f'Extra deps: {extra_deps}\n'
f'Hash for docker build directory [{dir_path}] (contents: {os.listdir(dir_path)}): {hash}\n'
)
return hash
def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
"""Retrieves the Docker repo and tag associated with the Docker image.
@@ -204,7 +99,7 @@ def build_runtime_image(
base_image: str,
runtime_builder: RuntimeBuilder,
extra_deps: str | None = None,
docker_build_folder: str | None = None,
build_folder: str | None = None,
dry_run: bool = False,
force_rebuild: bool = False,
) -> str:
@@ -215,7 +110,7 @@ def build_runtime_image(
- base_image (str): The name of the base Docker image to use
- runtime_builder (RuntimeBuilder): The runtime builder to use
- extra_deps (str):
- docker_build_folder (str): The directory to use for the build. If not provided a temporary directory will be used
- build_folder (str): The directory to use for the build. If not provided a temporary directory will be used
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
- force_rebuild (bool): if True, it will create the Dockerfile which uses the base_image
@@ -224,160 +119,195 @@ def build_runtime_image(
See https://docs.all-hands.dev/modules/usage/architecture/runtime for more details.
"""
# Calculate the hash for the docker build folder (source code and Dockerfile)
with tempfile.TemporaryDirectory() as temp_dir:
from_scratch_hash = prep_docker_build_folder(
temp_dir,
base_image=base_image,
skip_init=False,
extra_deps=extra_deps,
)
if build_folder is None:
with tempfile.TemporaryDirectory() as temp_dir:
result = build_runtime_image_in_folder(
base_image=base_image,
runtime_builder=runtime_builder,
build_folder=Path(temp_dir),
extra_deps=extra_deps,
dry_run=dry_run,
force_rebuild=force_rebuild,
)
return result
runtime_image_repo, runtime_image_tag = get_runtime_image_repo_and_tag(base_image)
result = build_runtime_image_in_folder(
base_image=base_image,
runtime_builder=runtime_builder,
build_folder=Path(build_folder),
extra_deps=extra_deps,
dry_run=dry_run,
force_rebuild=force_rebuild,
)
return result
# The image name in the format <image repo>:<hash>
hash_runtime_image_name = f'{runtime_image_repo}:{from_scratch_hash}'
# non-hash generic image name, it could contain *similar* dependencies
# but *might* not exactly match the state of the source code.
# It resembles the "latest" tag in the docker image naming convention for
# a particular {repo}:{tag} pair (e.g., ubuntu:latest -> runtime:ubuntu_tag_latest)
# we will build from IT to save time if the `from_scratch_hash` is not found
generic_runtime_image_name = f'{runtime_image_repo}:{runtime_image_tag}'
# Scenario 1: If we already have an image with the exact same hash, then it means the image is already built
# with the exact same source code and Dockerfile, so we will reuse it. Building it is not required.
if not force_rebuild and runtime_builder.image_exists(
hash_runtime_image_name, False
):
logger.info(
f'Image [{hash_runtime_image_name}] already exists so we will reuse it.'
)
return hash_runtime_image_name
# Scenario 2: If a Docker image with the exact hash is not found, we will FIRST try to re-build it
# by leveraging the `generic_runtime_image_name` to save some time
# from re-building the dependencies (e.g., poetry install, apt install)
if not force_rebuild and runtime_builder.image_exists(generic_runtime_image_name):
logger.info(
f'Could not find docker image [{hash_runtime_image_name}]\n'
f'Will try to re-build it from latest [{generic_runtime_image_name}] image to potentially save '
f'time for dependencies installation.\n'
)
cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
_skip_init_hash = prep_docker_build_folder(
cur_docker_build_folder,
# we want to use the existing generic image as base
# so that we can leverage existing dependencies already installed in the image
base_image=generic_runtime_image_name,
skip_init=True, # skip init since we are re-using the existing image
extra_deps=extra_deps,
)
assert (
_skip_init_hash != from_scratch_hash
), f'The skip_init hash [{_skip_init_hash}] should not match the existing hash [{from_scratch_hash}]'
def build_runtime_image_in_folder(
base_image: str,
runtime_builder: RuntimeBuilder,
build_folder: Path,
extra_deps: str | None,
dry_run: bool,
force_rebuild: bool,
) -> str:
runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image)
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image)}'
hash_tag = f'{lock_tag}_{get_hash_for_source_files()}'
hash_image_name = f'{runtime_image_repo}:{hash_tag}'
if force_rebuild:
logger.info(f'Force rebuild: [{runtime_image_repo}:{hash_tag}] from scratch.')
prep_build_folder(build_folder, base_image, True, extra_deps)
if not dry_run:
_build_sandbox_image(
docker_folder=cur_docker_build_folder,
runtime_builder=runtime_builder,
target_image_repo=runtime_image_repo,
# NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image
# otherwise, even if the source code is exactly the same, the image *might* be re-built
# because the same source code will generate different hash when skip_init=True/False
# since the Dockerfile is slightly different
target_image_hash_tag=from_scratch_hash,
target_image_tag=runtime_image_tag,
)
else:
logger.info(
f'Dry run: Skipping image build for [{generic_runtime_image_name}]'
build_folder,
runtime_builder,
runtime_image_repo,
hash_tag,
lock_tag,
)
return hash_image_name
if docker_build_folder is None:
shutil.rmtree(cur_docker_build_folder)
lock_image_name = f'{runtime_image_repo}:{lock_tag}'
build_from_scratch = True
# Scenario 3: If the Docker image with the required hash is not found AND we cannot re-use the latest
# relevant image, we will build it completely from scratch
# If the exact image already exists, we do not need to build it
if runtime_builder.image_exists(hash_image_name, False):
logger.info(f'Reusing Image [{hash_image_name}]')
return hash_image_name
# We look for an existing image that shares the same lock_tag. If such an image exists, we
# can use it as the base image for the build and just copy source files. This makes the build
# much faster.
if runtime_builder.image_exists(lock_image_name):
logger.info(f'Build [{hash_image_name}] from [{lock_image_name}]')
build_from_scratch = False
base_image = lock_image_name
else:
if force_rebuild:
logger.info(
f'Force re-build: Will try to re-build image [{generic_runtime_image_name}] from scratch.\n'
)
logger.info(f'Build [{hash_image_name}] from scratch')
cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
_new_from_scratch_hash = prep_docker_build_folder(
cur_docker_build_folder,
base_image,
skip_init=False,
extra_deps=extra_deps,
prep_build_folder(build_folder, base_image, build_from_scratch, extra_deps)
if not dry_run:
_build_sandbox_image(
build_folder,
runtime_builder,
runtime_image_repo,
hash_tag,
lock_tag,
)
assert (
_new_from_scratch_hash == from_scratch_hash
), f'The new from scratch hash [{_new_from_scratch_hash}] does not match the existing hash [{from_scratch_hash}]'
if not dry_run:
_build_sandbox_image(
docker_folder=cur_docker_build_folder,
runtime_builder=runtime_builder,
target_image_repo=runtime_image_repo,
# NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image
target_image_hash_tag=from_scratch_hash,
target_image_tag=runtime_image_tag,
)
else:
logger.info(
f'Dry run: Skipping image build for [{generic_runtime_image_name}]'
)
return hash_image_name
if docker_build_folder is None:
shutil.rmtree(cur_docker_build_folder)
return f'{runtime_image_repo}:{from_scratch_hash}'
def prep_build_folder(
build_folder: Path,
base_image: str,
build_from_scratch: bool,
extra_deps: str | None,
):
# Copy the source code to directory. It will end up in build_folder/code
# If package is not found, build from source code
openhands_source_dir = Path(openhands.__file__).parent
project_root = openhands_source_dir.parent
logger.info(f'Building source distribution using project root: {project_root}')
# Copy the 'openhands' directory (Source code)
shutil.copytree(
openhands_source_dir,
Path(build_folder, 'code', 'openhands'),
ignore=shutil.ignore_patterns(
'.*/',
'__pycache__/',
'*.pyc',
'*.md',
),
)
# Copy pyproject.toml and poetry.lock files
for file in ['pyproject.toml', 'poetry.lock']:
src = Path(openhands_source_dir, file)
if not src.exists():
src = Path(project_root, file)
shutil.copy2(src, Path(build_folder, 'code', file))
# Create a Dockerfile and write it to build_folder
dockerfile_content = _generate_dockerfile(
base_image,
build_from_scratch=build_from_scratch,
extra_deps=extra_deps,
)
with open(Path(build_folder, 'Dockerfile'), 'w') as file: # type: ignore
file.write(dockerfile_content) # type: ignore
_ALPHABET = string.digits + string.ascii_lowercase
def truncate_hash(hash: str) -> str:
"""Convert the base16 hash to base36 and truncate at 16 characters."""
value = int(hash, 16)
result: List[str] = []
while value > 0 and len(result) < 16:
value, remainder = divmod(value, len(_ALPHABET))
result.append(_ALPHABET[remainder])
return ''.join(result)
def get_hash_for_lock_files(base_image: str):
openhands_source_dir = Path(openhands.__file__).parent
md5 = hashlib.md5()
md5.update(base_image.encode())
for file in ['pyproject.toml', 'poetry.lock']:
src = Path(openhands_source_dir, file)
if not src.exists():
src = Path(openhands_source_dir.parent, file)
with open(src, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
md5.update(chunk)
# We get away with truncation because we want something that is unique
# rather than something that is cryptographically secure
result = truncate_hash(md5.hexdigest())
return result
def get_hash_for_source_files():
openhands_source_dir = Path(openhands.__file__).parent
dir_hash = dirhash(
openhands_source_dir,
'md5',
ignore=[
'.*/', # hidden directories
'__pycache__/',
'*.pyc',
],
)
# We get away with truncation because we want something that is unique
# rather than something that is cryptographically secure
result = truncate_hash(dir_hash)
return result
def _build_sandbox_image(
docker_folder: str,
build_folder: Path,
runtime_builder: RuntimeBuilder,
target_image_repo: str,
target_image_hash_tag: str,
target_image_tag: str,
) -> str:
"""Build and tag the sandbox image.
The image will be tagged as both:
- target_image_hash_tag
- target_image_tag
runtime_image_repo: str,
hash_tag: str,
lock_tag: str,
):
"""Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist"""
Parameters:
- docker_folder (str): the path to the docker build folder
- runtime_builder (RuntimeBuilder): the runtime builder instance
- target_image_repo (str): the repository name for the target image
- target_image_hash_tag (str): the *hash* tag for the target image that is calculated based
on the contents of the docker build folder (source code and Dockerfile)
e.g. 1234567890abcdef
-target_image_tag (str): the tag for the target image that's generic and based on the base image name
e.g. oh_v0.9.3_image_ubuntu_tag_22.04
"""
target_image_hash_name = f'{target_image_repo}:{target_image_hash_tag}'
target_image_generic_name = f'{target_image_repo}:{target_image_tag}'
names = [
name
for name in [
f'{runtime_image_repo}:{hash_tag}',
f'{runtime_image_repo}:{lock_tag}',
]
if not runtime_builder.image_exists(name, False)
]
tags_to_add = [target_image_hash_name]
# Only add the generic tag if the image does not exist
# so it does not get overwritten & only points to the earliest version
# to avoid "too many layers" after many re-builds
if not runtime_builder.image_exists(target_image_generic_name):
tags_to_add.append(target_image_generic_name)
try:
image_name = runtime_builder.build(path=docker_folder, tags=tags_to_add)
if not image_name:
raise RuntimeError(f'Build failed for image {target_image_hash_name}')
except Exception as e:
logger.error(f'Sandbox image build failed: {str(e)}')
raise
image_name = runtime_builder.build(path=str(build_folder), tags=names)
if not image_name:
raise RuntimeError(f'Build failed for image {names}')
return image_name
@@ -416,7 +346,7 @@ if __name__ == '__main__':
runtime_image_hash_name = build_runtime_image(
args.base_image,
runtime_builder=DockerRuntimeBuilder(docker.from_env()),
docker_build_folder=temp_dir,
build_folder=temp_dir,
dry_run=True,
force_rebuild=args.force_rebuild,
)
@@ -4,10 +4,13 @@ FROM {{ base_image }}
ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry
ENV MAMBA_ROOT_PREFIX=/openhands/micromamba
{% if not skip_init %}
{% if build_from_scratch %}
# ================================================================
# START: Build Runtime Image from Scratch
# ================================================================
# This is used in cases where the base image is something more generic like nikolaik/python-nodejs
# rather than the current OpenHands release
{% if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) %}
{% set LIBGL_MESA = 'libgl1' %}
{% else %}
@@ -38,24 +41,14 @@ RUN mkdir -p /openhands/micromamba/bin && \
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
/openhands/micromamba/bin/micromamba install -n openhands -c conda-forge poetry python=3.12 -y
# ================================================================
# END: Build Runtime Image from Scratch
# ================================================================
{% endif %}
# Create a clean openhands directory including only the pyproject.toml, poetry.lock and openhands/__init__.py
RUN \
if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \
mkdir -p /openhands/code/openhands && \
touch /openhands/code/openhands/__init__.py
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code
# ================================================================
# START: Copy Project and Install/Update Dependencies
# ================================================================
RUN if [ -d /openhands/code ]; then rm -rf /openhands/code; fi
COPY ./code /openhands/code
# Below RUN command sets up the Python environment using Poetry,
# installs project dependencies, and configures the container
# for OpenHands development.
# It creates and activates a virtual environment, installs necessary
# tools like Playwright, sets up environment variables, and configures
# the bash environment to ensure the correct Python interpreter and
# virtual environment are used by default.
# Install all dependencies
WORKDIR /openhands/code
RUN \
/openhands/micromamba/bin/micromamba config set changeps1 False && \
@@ -70,16 +63,26 @@ RUN \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \
# Set environment variables
echo "OH_INTERPRETER_PATH=$(/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print(sys.executable)")" >> /etc/environment && \
# Install extra dependencies if specified
{{ extra_deps }} {% if extra_deps %} && {% endif %} \
# Clear caches
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . && \
# Set permissions
{% if not skip_init %}chmod -R g+rws /openhands/poetry && {% endif %} \
chmod -R g+rws /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
# Clean up
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/micromamba/bin/micromamba clean --all
# ================================================================
# END: Copy Project and Install/Update Dependencies
# END: Build Runtime Image from Scratch
# ================================================================
{% endif %}
# ================================================================
# Copy Project source files
# ================================================================
RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code
COPY ./code/openhands /openhands/code/openhands
# Install extra dependencies if specified
{% if extra_deps %}RUN {{ extra_deps }} {% endif %}
+1 -1
View File
@@ -140,7 +140,7 @@ class AgentSession:
await self.security_analyzer.close()
if self.loop:
self.loop.call_soon_threadsafe(self.loop.stop)
self.loop.stop()
self._closed = True
Generated
+41 -41
View File
@@ -209,13 +209,13 @@ files = [
[[package]]
name = "anthropic"
version = "0.36.0"
version = "0.36.1"
description = "The official Python library for the anthropic API"
optional = false
python-versions = ">=3.7"
files = [
{file = "anthropic-0.36.0-py3-none-any.whl", hash = "sha256:9183b9eaa0f409f2047244d7ef02c9c3eb916959c0b2960f7605dcb6cabbf548"},
{file = "anthropic-0.36.0.tar.gz", hash = "sha256:7b0b1457096605572a29559d9a8ce224b9389d379b410e7d1bf5e0c1379f9ee2"},
{file = "anthropic-0.36.1-py3-none-any.whl", hash = "sha256:908968f89ecdf9747c34cf632e2099668ee515a38293d455ef7ad79a3d4f527c"},
{file = "anthropic-0.36.1.tar.gz", hash = "sha256:8a343caa335aefc3ce64e2e559c88ca01af409ace5d8c4f83bd0b586315a43a2"},
]
[package.dependencies]
@@ -585,17 +585,17 @@ files = [
[[package]]
name = "boto3"
version = "1.35.40"
version = "1.35.42"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "boto3-1.35.40-py3-none-any.whl", hash = "sha256:9352f6d61f15c789231a5d608613f03425059072ed862c32e1ed102b17206abf"},
{file = "boto3-1.35.40.tar.gz", hash = "sha256:33c6a7aeab316f7e0b3ad8552afe95a4a10bfd58519d00741c4d4f3047da8382"},
{file = "boto3-1.35.42-py3-none-any.whl", hash = "sha256:e1f36f8be453505cebcc3da178ea081b2a06c0e5e1cdee774f1067599b8d9c3e"},
{file = "boto3-1.35.42.tar.gz", hash = "sha256:a5b00f8b82dce62870759f04861747944da834d64a64355970120c475efdafc0"},
]
[package.dependencies]
botocore = ">=1.35.40,<1.36.0"
botocore = ">=1.35.42,<1.36.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -604,13 +604,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.35.40"
version = "1.35.42"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
{file = "botocore-1.35.40-py3-none-any.whl", hash = "sha256:072cc47f29cb1de4fa77ce6632e4f0480af29b70816973ff415fbaa3f50bd1db"},
{file = "botocore-1.35.40.tar.gz", hash = "sha256:547e0a983856c7d7aeaa30fca2a283873c57c07366cd806d2d639856341b3c31"},
{file = "botocore-1.35.42-py3-none-any.whl", hash = "sha256:05af0bb8b9cea7ce7bc589c332348d338a21b784e9d088a588fd10ec145007ff"},
{file = "botocore-1.35.42.tar.gz", hash = "sha256:af348636f73dc24b7e2dc760a34d08c8f2f94366e9b4c78d877307b128abecef"},
]
[package.dependencies]
@@ -623,32 +623,32 @@ crt = ["awscrt (==0.22.0)"]
[[package]]
name = "browsergym"
version = "0.8.0"
version = "0.8.1"
description = "BrowserGym: a gym environment for web task automation in the Chromium browser"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym-0.8.0-py3-none-any.whl", hash = "sha256:c66f3f042af06451d09e3f8d3d2a331f41048ecdfa5b51980d1f929e96aacd84"},
{file = "browsergym-0.8.0.tar.gz", hash = "sha256:d6522cf8ac905907891943f60176d9ef984eba6d209bf7e41eee4463ff1bed08"},
{file = "browsergym-0.8.1-py3-none-any.whl", hash = "sha256:b194cb4f52f439fff571351b045330171908a3d291e2a5527097461f694074d1"},
{file = "browsergym-0.8.1.tar.gz", hash = "sha256:1fa9bb47b236cb2e36ef4d931b7aa6fa30bb73fc0f87ad7ba9da86abfdf73bbb"},
]
[package.dependencies]
browsergym-core = "0.8.0"
browsergym-experiments = "0.8.0"
browsergym-miniwob = "0.8.0"
browsergym-visualwebarena = "0.8.0"
browsergym-webarena = "0.8.0"
browsergym-core = "0.8.1"
browsergym-experiments = "0.8.1"
browsergym-miniwob = "0.8.1"
browsergym-visualwebarena = "0.8.1"
browsergym-webarena = "0.8.1"
browsergym-workarena = "*"
[[package]]
name = "browsergym-core"
version = "0.8.0"
version = "0.8.1"
description = "BrowserGym: a gym environment for web task automation in the Chromium browser"
optional = false
python-versions = ">3.9"
files = [
{file = "browsergym_core-0.8.0-py3-none-any.whl", hash = "sha256:9f949246c077a2fe1b5a02f65665beefbc17ae14a73397b75b7e7bf037f2aaba"},
{file = "browsergym_core-0.8.0.tar.gz", hash = "sha256:8102c79a10aadd0c5195f570ed35b61845bcf8c6aea0ae66956e7a27ee6136e3"},
{file = "browsergym_core-0.8.1-py3-none-any.whl", hash = "sha256:80f085d330f3f6a31f38233aa4b1030a0fb9fc87493f628f16cb1b0276ceb8fc"},
{file = "browsergym_core-0.8.1.tar.gz", hash = "sha256:802df5d75e56c362fd32f48fc9819374729195a9452d956809fa8864930a2381"},
]
[package.dependencies]
@@ -662,62 +662,62 @@ pyparsing = ">=3"
[[package]]
name = "browsergym-experiments"
version = "0.8.0"
version = "0.8.1"
description = "Experimentation tools for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_experiments-0.8.0-py3-none-any.whl", hash = "sha256:52fe37013212ae2dbdf6276e5bc46d6a6271405e601dd8d9d72fb838a0208f47"},
{file = "browsergym_experiments-0.8.0.tar.gz", hash = "sha256:fef7e047ff06d961ae79baa89b69cccd8b3ea7781fddeadc20e7a504c32ef5e0"},
{file = "browsergym_experiments-0.8.1-py3-none-any.whl", hash = "sha256:49f51f93dfb246fe34ba2c7c7b9b7207c4c47b3c13f27d2200a954834a841bb4"},
{file = "browsergym_experiments-0.8.1.tar.gz", hash = "sha256:32c5fd96dab3dd6271128699058e003ea39a39f27a0e57ae3c593b0b2de5f3d1"},
]
[package.dependencies]
browsergym-core = "0.8.0"
browsergym-core = "0.8.1"
tiktoken = ">=0.4"
[[package]]
name = "browsergym-miniwob"
version = "0.8.0"
version = "0.8.1"
description = "MiniWoB++ benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_miniwob-0.8.0-py3-none-any.whl", hash = "sha256:cfe54e1d58dab5e8524705cbbfabbe656cdff88f01fba6e253c8451aa7fabea9"},
{file = "browsergym_miniwob-0.8.0.tar.gz", hash = "sha256:617c73af493a7c707d395ca750ede5aa17f1015488dea9abe5823f0a22112d5d"},
{file = "browsergym_miniwob-0.8.1-py3-none-any.whl", hash = "sha256:089b920808f8189e82398c0ccafcb9860bcad434385eb8fe1166f60f8b13df86"},
{file = "browsergym_miniwob-0.8.1.tar.gz", hash = "sha256:ec5d14f9a3b8f5fbf7843a145268d6071085b232ebe5207f4693cb0d8e2c142b"},
]
[package.dependencies]
browsergym-core = "0.8.0"
browsergym-core = "0.8.1"
[[package]]
name = "browsergym-visualwebarena"
version = "0.8.0"
version = "0.8.1"
description = "VisualWebArena benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_visualwebarena-0.8.0-py3-none-any.whl", hash = "sha256:19342f512296739bfb87526944d1d687185f0292bd126cef66abac64df595bef"},
{file = "browsergym_visualwebarena-0.8.0.tar.gz", hash = "sha256:551a899239a94d3ffa687a925d78f60481b6cc838bce25ab5741c2f70537d22e"},
{file = "browsergym_visualwebarena-0.8.1-py3-none-any.whl", hash = "sha256:1119943e35b067759b4f8c154e9225d3a243d833771d399f60ecd46be2d32440"},
{file = "browsergym_visualwebarena-0.8.1.tar.gz", hash = "sha256:799b36cf33e0fb706c04c7bfad972c91d6827733b211cb3ad31a5734906e778a"},
]
[package.dependencies]
browsergym-core = "0.8.0"
browsergym-core = "0.8.1"
libvisualwebarena = "0.0.14"
requests = "*"
[[package]]
name = "browsergym-webarena"
version = "0.8.0"
version = "0.8.1"
description = "WebArena benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_webarena-0.8.0-py3-none-any.whl", hash = "sha256:e5bac8f812f816b5329deed17d410359c70763e74529131d3e306c8cbcf0c7da"},
{file = "browsergym_webarena-0.8.0.tar.gz", hash = "sha256:8d62a470e44d4973fcaf6cbaa7d5dda0fdd6f718274e039b6274abbb59684aee"},
{file = "browsergym_webarena-0.8.1-py3-none-any.whl", hash = "sha256:73cfe0968c4fc26d0ce4f2630bb966c721c68352e50506c196a60c84f405e2f8"},
{file = "browsergym_webarena-0.8.1.tar.gz", hash = "sha256:f542155a9f7c5973406e8933f17d5a495693b438c8d3b379995751f9b47cf013"},
]
[package.dependencies]
browsergym-core = "0.8.0"
browsergym-core = "0.8.1"
libwebarena = "0.0.3"
[[package]]
@@ -3879,13 +3879,13 @@ types-tqdm = "*"
[[package]]
name = "litellm"
version = "1.49.4"
version = "1.49.6"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
files = [
{file = "litellm-1.49.4-py3-none-any.whl", hash = "sha256:3094a9f74979da993f4b3298372ec4416f7a3f82d11a0831c9c616098b3fb50a"},
{file = "litellm-1.49.4.tar.gz", hash = "sha256:5f16d40bfa7747fcc21f45f340454c57cbc705178244fe7326abac7c0759e05e"},
{file = "litellm-1.49.6-py3-none-any.whl", hash = "sha256:c3b1dc25861850e8c2409e14198325019fe22680bf0c455e0124c23e6b5de318"},
{file = "litellm-1.49.6.tar.gz", hash = "sha256:cd82b332e11d80bd7e8b866b407bfbda8afe8bcf109687be5747f0f85f09c74a"},
]
[package.dependencies]
@@ -10001,4 +10001,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "30e14c2cf941629d564c24acb03b10a68248e8a7101e9810a72fa83ff468bdd4"
content-hash = "f0d6c96fb36fd6ff330f27b0bf8a2051099b1c46f8e3b03d0d530025c87c92af"
+1 -1
View File
@@ -25,7 +25,7 @@ uvicorn = "*"
types-toml = "*"
numpy = "*"
json-repair = "*"
browsergym = "0.8.0" # integrate browsergym as the browsing interface
browsergym = "0.8.1" # integrate browsergym as the browsing interface
html2text = "*"
e2b = "^0.17.1"
pexpect = "*"
-171
View File
@@ -1,171 +0,0 @@
# Introduction
This folder contains backend integration tests that rely on a mock LLM. It serves
two purposes:
1. Ensure the quality of development, including OpenHands framework and agents.
2. Help contributors learn the workflow of OpenHands, and examples of real interactions
with (powerful) LLM, without spending real money.
## Why don't we launch an open-source model, e.g. LLAMA3?
There are two reasons:
1. LLMs cannot guarantee determinism, meaning the test behavior might change.
2. CI machines are not powerful enough to run any LLM that is sophisticated enough
to finish the tasks defined in tests.
Note: integration tests are orthogonal to evaluations/benchmarks
as they serve different purposes. Although benchmarks could also
capture bugs, some of which may not be caught by tests, benchmarks
require real LLMs which are non-deterministic and costly.
We run integration test suite for every single commit, which is
not possible with benchmarks.
## Known limitations
1. To avoid the potential impact of non-determinism, we remove all special
characters when doing the comparison. If two prompts for the same task only
differ in non-alphanumeric characters, a wrong mock response might be picked up.
2. It is required that everything has to be deterministic. For example, agent
must not use randomly generated numbers.
The folder is organised as follows:
```
├── README.md
├── conftest.py
├── mock
├── [RuntimeType]
│   | ├── [AgentName]
│   │   └── [TestName]
│   │   ├── prompt_*.log
│   │   ├── response_*.log
└── [TestFiles].py
```
where `conftest.py` defines the infrastructure needed to load real-world LLM prompts
and responses for mocking purpose. Prompts and responses generated during real runs
of agents with real LLMs are stored under `mock/AgentName/TestName` folders.
## Run Integration Tests
[ghcr_runtime.yml](../../.github/workflows/ghcr_runtime.yml) runs integration tests in a CI environment.
*Note:* If you are using docker desktop make sure that your version is up to date and "Enable Host Networking"
is checked (Under settings -> Resources -> Network ). Otherwise the integration tests may hang with the
message `Getting container logs...` repeated ad infinitum.
You can run:
```bash
# for event stream
TEST_RUNTIME=eventstream TEST_ONLY=true ./tests/integration/regenerate.sh
```
to run all integration tests until the first failure occurs.
If you'd only plan to run a specific test, set environment variable
`ONLY_TEST_NAME` to the actual test name. If you only want to run a specific agent,
set environment variable `ONLY_TEST_AGENT` to the agent. You could also use both,
e.g.
```bash
TEST_ONLY=true ONLY_TEST_NAME="test_simple_task_rejection" ONLY_TEST_AGENT="ManagerAgent" ./tests/integration/regenerate.sh
```
## Regenerate Integration Tests
When you make changes to an agent's prompt, the integration tests will fail. You'll need to regenerate them
by running the following command from OpenHands's project root directory:
```bash
TEST_RUNTIME=eventstream ./tests/integration/regenerate.sh
```
Please note that this will:
1. Run existing tests first. If a test passes, then no regeneration would happen.
2. Regenerate the prompts, but attempt to use existing responses from LLM (if any).
For example, if you only fix a typo in the prompt, it shouldn't affect LLM's behaviour.
If we rerun integration tests against a real LLM, then due to LLM's non-deterministic nature,
a series of different prompts and responses will be generated, causing a lot of
unnecessary diffs which are hard to review. If you want to skip this step, see below
sections.
3. Rerun the failed test again. If it succeeds, continue to the next test or agent.
If it fails again, goto next step.
4. Rerun integration tests against a real LLM, record all prompts and
responses, and replace the existing test artifacts (if any).
5. Rerun the failed test again. If it succeeds, continue to the next test or agent.
If it fails again, abort the script.
Note that step 4 calls *real* LLM_MODEL only for failed tests that cannot be fixed
by regenerating prompts alone, but it still costs money! If you don't want
to cover the cost, ask one of the maintainers to regenerate for you. Before asking,
please try running the script first *without* setting `LLM_API_KEY`.
Chance is, the test could be fixed after step 2.
## Regenerate Integration Tests without testing first
If you want to regenerate all prompts and/or responses without running the existing tests first, you can run:
```bash
FORCE_REGENERATE=true ./tests/integration/regenerate.sh
```
This will skip the first step and directly regenerate all tests when you know that the tests will fail due to changes in the prompt or the agent code itself and will save time.
## Regenerate a Specific Agent and/or Test
If you only want to run a specific test, set environment variable
`ONLY_TEST_NAME` to the test name. If you only want to run a specific agent,
set environment variable `ONLY_TEST_AGENT` to the agent. You could also use both,
e.g.
```bash
ONLY_TEST_NAME="test_write_simple_script" ONLY_TEST_AGENT="CodeActAgent" ./tests/integration/regenerate.sh
```
## Force Regenerate with real LLM
Sometimes, step 2 would fix the broken test by simply reusing existing responses
from LLM. This may not be what you want - for example, you might have greatly improved
the prompt that you believe the LLM will do a better job using fewer steps, or you might
have added a new action type and you think the LLM should be able to use the new type.
In this case you can skip step 2 and run integration tests against a real LLM.
Simply set `FORCE_USE_LLM` environmental variable to true, or run the script like this:
```bash
FORCE_USE_LLM=true ./tests/integration/regenerate.sh
```
Note: `FORCE_USE_LLM` doesn't take effect if all tests are passing. If you want to
regenerate regardless, you could remove everything under the
`tests/integration/mock/[agent]/[test_name]` folder.
## Known Issues
The test framework cannot handle non-determinism. If anything in the prompt (including
observed result after executing an action) is non-deterministic (e.g. a PID), the
test would fail. In this case, you might want to change conftest.py to filter out
numbers or any other known patterns when matching prompts for your test.
## Write a new Integration Test
To write an integration test, there are essentially two steps:
1. Decide your task prompt, and the result you want to verify.
2. Add your prompt to the `regenerate.sh` script.
**NOTE**: If your agent decides to support user-agent interaction via natural
language (e.g., you're prompted to enter user responses when running the above
`main.py` command), you should create a file named
`tests/integration/mock/<AgentName>/<TestName>/user_responses.log`
containing all the responses in order you provided to the agent,
delimited by a single newline ('\n'). This will be used to mock the STDIN during testing.
That's it, you are good to go! When you launch an integration test, mock
responses are loaded and used to replace a real LLM's response, so that we get
deterministic and consistent behavior, and most importantly, without spending real
money.
-312
View File
@@ -1,312 +0,0 @@
import io
import os
import re
import shutil
import socket
import subprocess
import tempfile
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
import pytest
from litellm import completion
from openhands.llm.debug_mixin import MESSAGE_SEPARATOR
script_dir = os.environ.get('SCRIPT_DIR')
project_root = os.environ.get('PROJECT_ROOT')
workspace_path = os.environ.get('WORKSPACE_BASE')
test_runtime = os.environ.get('TEST_RUNTIME')
MOCK_ROOT_DIR = os.path.join(
script_dir,
'mock',
f'{test_runtime}_runtime',
os.environ.get('DEFAULT_AGENT'),
)
assert script_dir is not None, 'SCRIPT_DIR environment variable is not set'
assert project_root is not None, 'PROJECT_ROOT environment variable is not set'
assert workspace_path is not None, 'WORKSPACE_BASE environment variable is not set'
assert test_runtime is not None, 'TEST_RUNTIME environment variable is not set'
class SecretExit(Exception):
pass
@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact(node, call, report):
if isinstance(call.excinfo.value, SecretExit):
report.outcome = 'failed'
report.longrepr = (
'SecretExit: Exiting due to an error without revealing secrets.'
)
call.excinfo = None
def filter_out_symbols(input):
# remove shell hostname patterns (e.g., will change between each run)
# openhands@379c7fce40b4:/workspace $
input = re.sub(r'(openhands|root)@.*(:/.*)', r'\1[DUMMY_HOSTNAME]\2', input)
# mask the specific part in a poetry path
input = re.sub(
r'(/open[a-z]{5}/poetry/open[a-z]{5}-)[a-zA-Z0-9-]+(-py3\.\d+/bin/python)',
r'\1[DUMMY_STRING]\2',
input,
)
# handle size param
input = re.sub(r' size=\d+ ', ' size=[DUMMY_SIZE] ', input)
# handle sha256 hashes
# sha256=4ecf8be428f55981e2a188f510ba5f9022bed88f5fb404d7d949f44382201e3d
input = re.sub(r'sha256=[a-z0-9]+', 'sha256=[DUMMY_HASH]', input)
# remove newlines and whitespace
input = re.sub(r'\\n|\\r\\n|\\r|\s+', '', input)
# remove all non-alphanumeric characters
input = re.sub(r'[^a-zA-Z0-9]', '', input)
return input
def get_log_id(prompt_log_name):
match = re.search(r'prompt_(\d+).log', prompt_log_name)
if match:
return match.group(1)
def _format_messages(messages):
message_str = ''
for message in messages:
if isinstance(message, str):
message_str += MESSAGE_SEPARATOR + message if message_str else message
elif isinstance(message, dict):
if isinstance(message['content'], list):
for m in message['content']:
if isinstance(m, str):
message_str += MESSAGE_SEPARATOR + m if message_str else m
elif isinstance(m, dict) and m['type'] == 'text':
message_str += (
MESSAGE_SEPARATOR + m['text'] if message_str else m['text']
)
elif isinstance(message['content'], str):
message_str += (
MESSAGE_SEPARATOR + message['content']
if message_str
else message['content']
)
return message_str
def apply_prompt_and_get_mock_response(
test_name: str, messages: str, id: int
) -> str | None:
"""Apply the mock prompt, and find mock response based on id.
If there is no matching response file, return None.
Note: this function blindly replaces existing prompt file with the given
input without checking the contents.
"""
mock_dir = os.path.join(MOCK_ROOT_DIR, test_name)
prompt_file_path = os.path.join(mock_dir, f'prompt_{"{0:03}".format(id)}.log')
resp_file_path = os.path.join(mock_dir, f'response_{"{0:03}".format(id)}.log')
try:
# load response
with open(resp_file_path, 'r') as resp_file:
response = resp_file.read()
# apply prompt
with open(prompt_file_path, 'w') as prompt_file:
prompt_file.write(messages)
prompt_file.write('\n')
return response
except FileNotFoundError:
return None
def get_mock_response(test_name: str, messages: str, id: int) -> str:
"""Find mock response based on prompt. Prompts are stored under nested
folders under mock folder. If prompt_{id}.log matches,
then the mock response we're looking for is at response_{id}.log.
Note: we filter out all non-alphanumerical characters, otherwise we would
see surprising mismatches caused by linters and minor discrepancies between
different platforms.
We could have done a slightly more efficient string match with the same time
complexity (early-out upon first character mismatch), but it is unnecessary
for tests. Empirically, different prompts of the same task usually only
differ near the end of file, so the comparison would be more efficient if
we start from the end of the file, but again, that is unnecessary and only
makes test code harder to understand.
"""
mock_dir = os.path.join(MOCK_ROOT_DIR, test_name)
prompt = filter_out_symbols(messages)
prompt_file_path = os.path.join(mock_dir, f'prompt_{"{0:03}".format(id)}.log')
resp_file_path = os.path.join(mock_dir, f'response_{"{0:03}".format(id)}.log')
# Open the prompt file and compare its contents
with open(prompt_file_path, 'r') as f:
file_content = filter_out_symbols(f.read())
if file_content.strip() == prompt.strip():
# Read the response file and return its content
with open(resp_file_path, 'r') as resp_file:
return resp_file.read()
else:
# print the mismatched lines
print('Mismatched Prompt File path', prompt_file_path)
print('---' * 10)
# Create a temporary file to store messages
with tempfile.NamedTemporaryFile(
delete=False, mode='w', encoding='utf-8'
) as tmp_file:
tmp_file_path = tmp_file.name
tmp_file.write(messages)
try:
# Use diff command to compare files and capture the output
result = subprocess.run(
['diff', '-u', prompt_file_path, tmp_file_path],
capture_output=True,
text=True,
)
if result.returncode != 0:
print('Diff:')
print(result.stdout)
else:
print('No differences found.')
finally:
# Clean up the temporary file
os.remove(tmp_file_path)
print('---' * 10)
def mock_user_response(*args, test_name, **kwargs):
"""The agent will ask for user input using `input()` when calling `asyncio.run(main(task))`.
This function mocks the user input by providing the response from the mock response file.
It will read the `user_responses.log` file in the test directory and set as
STDIN input for the agent to read.
"""
user_response_file = os.path.join(
script_dir,
'mock',
os.environ.get('DEFAULT_AGENT'),
test_name,
'user_responses.log',
)
if not os.path.exists(user_response_file):
return ''
with open(user_response_file, 'r') as f:
ret = f.read().rstrip()
ret += '\n'
return ret
def mock_completion(*args, test_name, **kwargs):
global cur_id
messages = kwargs['messages']
message_str = _format_messages(messages) # text only
# this assumes all response_(*).log filenames are in numerical order, starting from one
cur_id += 1
if os.environ.get('FORCE_APPLY_PROMPTS') == 'true':
mock_response = apply_prompt_and_get_mock_response(
test_name, message_str, cur_id
)
else:
mock_response = get_mock_response(test_name, message_str, cur_id)
if mock_response is None:
raise SecretExit('\n\n***** Mock response for prompt is not found *****\n')
response = completion(**kwargs, mock_response=mock_response)
return response
@pytest.fixture
def current_test_name(request):
return request.node.name
@pytest.fixture(autouse=True)
def patch_completion(monkeypatch, request):
test_name = request.node.name
# Mock LLM completion
monkeypatch.setattr(
'openhands.llm.llm.litellm_completion',
partial(mock_completion, test_name=test_name),
)
# Mock LLM completion cost (1 USD per conversation)
monkeypatch.setattr(
'openhands.llm.llm.litellm_completion_cost',
lambda completion_response, **extra_kwargs: 1,
)
# Mock LLMConfig to disable vision support
monkeypatch.setattr(
'openhands.llm.llm.LLM.vision_is_active',
lambda self: False,
)
# Mock user input (only for tests that have user_responses.log)
user_responses_str = mock_user_response(test_name=test_name)
if user_responses_str:
user_responses = io.StringIO(user_responses_str)
monkeypatch.setattr('sys.stdin', user_responses)
class MultiAddressServer(HTTPServer):
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
class LoggingHTTPRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
print(
f'Request received: {self.address_string()} - {self.log_date_time_string()} - {format % args}'
)
def set_up():
global cur_id
cur_id = 0
assert workspace_path is not None, 'workspace_path is not set'
# Remove and recreate the workspace_path
if os.path.exists(workspace_path):
shutil.rmtree(workspace_path)
os.makedirs(workspace_path)
@pytest.fixture(autouse=True)
def resource_setup():
try:
original_cwd = os.getcwd()
except FileNotFoundError:
print(
'[DEBUG] Original working directory does not exist. Using /tmp as fallback.'
)
original_cwd = '/tmp'
os.chdir('/tmp')
try:
set_up()
yield
finally:
try:
print(f'[DEBUG] Final working directory: {os.getcwd()}')
except FileNotFoundError:
print('[DEBUG] Final working directory does not exist')
if os.path.exists(workspace_path):
shutil.rmtree(workspace_path)
os.makedirs(workspace_path, exist_ok=True)
# Try to change back to the original directory
try:
os.chdir(original_cwd)
print(f'[DEBUG] Changed back to original directory: {original_cwd}')
except Exception:
os.chdir('/tmp')
@@ -1,124 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
# Current Accessibility Tree:
# Previous Actions
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,128 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
http://localhost:8000/
# Current Accessibility Tree:
RootWebArea 'The Ultimate Answer', focused
[8] heading 'The Ultimate Answer'
[9] paragraph ''
StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
[10] button 'Click me', clickable
# Previous Actions
goto("http://localhost:8000")
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,130 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
http://localhost:8000/
# Current Accessibility Tree:
RootWebArea 'The Ultimate Answer', focused
[8] heading 'The Ultimate Answer'
[9] paragraph ''
StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
[10] button 'Click me', clickable, focused
StaticText 'The answer is OpenHands is all you need!'
# Previous Actions
goto("http://localhost:8000")
click("10")
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,3 +0,0 @@
To accomplish the goal of browsing localhost:8000 and finding the ultimate answer to life, I need to first navigate to the specified URL.
```goto("http://localhost:8000"
@@ -1,4 +0,0 @@
To accomplish the goal of finding the ultimate answer to life, I need to click on the "Click me" button to reveal the answer. The button has a bid of 10.
```
click("10"
@@ -1,6 +0,0 @@
Based on the current Accessibility Tree and the previous actions taken, I can see that we have already navigated to the localhost:8000 page and clicked the "Click me" button. The answer to life, the universe, and everything has been revealed on the page.
To accomplish the goal of telling you the ultimate answer to life, I will send a message to the user with the revealed answer.
```
send_msg_to_user("The ultimate answer to life, according to the webpage, is: OpenHands is all you need!"
@@ -1,408 +0,0 @@
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
ENVIRONMENT REMINDER: You have 19 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,124 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Get the content on "http://localhost:8000"
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
# Current Accessibility Tree:
# Previous Actions
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,128 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Get the content on "http://localhost:8000"
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
http://localhost:8000/
# Current Accessibility Tree:
RootWebArea 'The Ultimate Answer', focused
[8] heading 'The Ultimate Answer'
[9] paragraph ''
StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
[10] button 'Click me', clickable
# Previous Actions
goto("http://localhost:8000")
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,130 +0,0 @@
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
Get the content on "http://localhost:8000"
# Action Space
16 different types of actions are available.
noop(wait_ms: float = 1000)
Examples:
noop()
noop(500)
send_msg_to_user(text: str)
Examples:
send_msg_to_user('Based on the results of my search, the city was built in 1751.')
scroll(delta_x: float, delta_y: float)
Examples:
scroll(0, 200)
scroll(-50.2, -100.5)
fill(bid: str, value: str)
Examples:
fill('237', 'example value')
fill('45', 'multi-line\nexample')
fill('a12', 'example with "quotes"')
select_option(bid: str, options: str | list[str])
Examples:
select_option('a48', 'blue')
select_option('c48', ['red', 'green', 'blue'])
click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
click('a51')
click('b22', button='right')
click('48', button='middle', modifiers=['Shift'])
dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
Examples:
dblclick('12')
dblclick('ca42', button='right')
dblclick('178', button='middle', modifiers=['Shift'])
hover(bid: str)
Examples:
hover('b8')
press(bid: str, key_comb: str)
Examples:
press('88', 'Backspace')
press('a26', 'Control+a')
press('a61', 'Meta+Shift+t')
focus(bid: str)
Examples:
focus('b455')
clear(bid: str)
Examples:
clear('996')
drag_and_drop(from_bid: str, to_bid: str)
Examples:
drag_and_drop('56', '498')
upload_file(bid: str, file: str | list[str])
Examples:
upload_file('572', 'my_receipt.pdf')
upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
go_back()
Examples:
go_back()
go_forward()
Examples:
go_forward()
goto(url: str)
Examples:
goto('http://www.example.com')
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
----------
# Current Page URL:
http://localhost:8000/
# Current Accessibility Tree:
RootWebArea 'The Ultimate Answer', focused
[8] heading 'The Ultimate Answer'
[9] paragraph ''
StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
[10] button 'Click me', clickable, focused
StaticText 'The answer is OpenHands is all you need!'
# Previous Actions
goto("http://localhost:8000")
click("10")
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
@@ -1,420 +0,0 @@
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
----------
I should start with: Get the content on "http://localhost:8000"
<execute_browse>
Get the content on "http://localhost:8000"
</execute_browse>
----------
OBSERVATION:
The answer to life, the universe, and everything has been revealed: OpenHands is all you need!
ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,2 +0,0 @@
<execute_browse>
Get the content on "http://localhost:8000"
@@ -1,5 +0,0 @@
To accomplish my goal of getting the content on "http://localhost:8000", I need to use the goto action to navigate to that URL. Here's the action with my chain of thought:
In order to accomplish my goal of accessing the content on "http://localhost:8000", I need to navigate to that URL using the goto action.
```goto("http://localhost:8000"
@@ -1,6 +0,0 @@
To accomplish the goal of revealing the answer, I need to click on the "Click me" button. The button has a bid of 10.
Here's the action I'll take:
In order to accomplish our goal, I need to click on the button with bid 10
```click("10"
@@ -1,10 +0,0 @@
Based on the current Accessibility Tree and the previous actions taken, it appears that we have already navigated to the correct page and clicked the button to reveal the answer. The answer is now visible on the page.
To accomplish our goal of retrieving the ultimate answer to life, I will now send a message to the user with the revealed answer:
Since the goal was to get the content on the page and reveal the answer, we have successfully completed the task. There's no need for any further actions.
However, to formally conclude and report our findings to the user, we should send a message summarizing what we've discovered. Here's the action I propose:
In order to report our findings to the user, I will send a message with the revealed answer.
```send_msg_to_user("The answer to life, the universe, and everything has been revealed: OpenHands is all you need!"
@@ -1,7 +0,0 @@
Based on the content retrieved from localhost:8000, I can provide you with the ultimate answer to life:
The ultimate answer to life, the universe, and everything is: OpenHands is all you need!
This appears to be the answer provided by the server at localhost:8000. It's a playful reference to Douglas Adams' "The Hitchhiker's Guide to the Galaxy," where the answer to life, the universe, and everything is famously 42. In this case, it's been customized to highlight OpenHands.
<finish></finish>
@@ -1,411 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
ENVIRONMENT REMINDER: You have 19 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,431 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
Certainly! I'll fix the typos in the bad.txt file. Let's start by opening the file and examining its contents.
<execute_ipython>
open_file('bad.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total)]
(this is the beginning of the file)
1|This is a stupid typoo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 18 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,462 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
Certainly! I'll fix the typos in the bad.txt file. Let's start by opening the file and examining its contents.
<execute_ipython>
open_file('bad.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total)]
(this is the beginning of the file)
1|This is a stupid typoo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
I'll now fix the typos in the bad.txt file. There are three typos to correct:
1. "typoo" should be "typo"
2. "stupid" is not appropriate, we'll change it to "silly"
3. "mor" should be "more"
Let's make these changes:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="This is a stupid typoo.",
new_content="This is a silly typo."
)
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total after edit)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 17 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,487 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
Certainly! I'll fix the typos in the bad.txt file. Let's start by opening the file and examining its contents.
<execute_ipython>
open_file('bad.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total)]
(this is the beginning of the file)
1|This is a stupid typoo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
I'll now fix the typos in the bad.txt file. There are three typos to correct:
1. "typoo" should be "typo"
2. "stupid" is not appropriate, we'll change it to "silly"
3. "mor" should be "more"
Let's make these changes:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="This is a stupid typoo.",
new_content="This is a silly typo."
)
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total after edit)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Now, let's fix the remaining typo:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="No mor typos!",
new_content="No more typos!"
)
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total after edit)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No more typos!
4|Enjoy!
(this is the end of the file)
[File updated (edited at line 3). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 16 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,507 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
Certainly! I'll fix the typos in the bad.txt file. Let's start by opening the file and examining its contents.
<execute_ipython>
open_file('bad.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total)]
(this is the beginning of the file)
1|This is a stupid typoo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
I'll now fix the typos in the bad.txt file. There are three typos to correct:
1. "typoo" should be "typo"
2. "stupid" is not appropriate, we'll change it to "silly"
3. "mor" should be "more"
Let's make these changes:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="This is a stupid typoo.",
new_content="This is a silly typo."
)
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total after edit)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No mor typos!
4|Enjoy!
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Now, let's fix the remaining typo:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="No mor typos!",
new_content="No more typos!"
)
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total after edit)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No more typos!
4|Enjoy!
(this is the end of the file)
[File updated (edited at line 3). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
The typos in the bad.txt file have been successfully corrected. Let's verify the final content of the file:
<execute_ipython>
open_file('bad.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/bad.txt (4 lines total)]
(this is the beginning of the file)
1|This is a silly typo.
2|Really?
3|No more typos!
4|Enjoy!
(this is the end of the file)
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 15 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,4 +0,0 @@
Certainly! I'll fix the typos in the bad.txt file. Let's start by opening the file and examining its contents.
<execute_ipython>
open_file('bad.txt')
@@ -1,14 +0,0 @@
I'll now fix the typos in the bad.txt file. There are three typos to correct:
1. "typoo" should be "typo"
2. "stupid" is not appropriate, we'll change it to "silly"
3. "mor" should be "more"
Let's make these changes:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="This is a stupid typoo.",
new_content="This is a silly typo."
)
@@ -1,8 +0,0 @@
Now, let's fix the remaining typo:
<execute_ipython>
edit_file_by_replace(
'bad.txt',
to_replace="No mor typos!",
new_content="No more typos!"
)
@@ -1,4 +0,0 @@
The typos in the bad.txt file have been successfully corrected. Let's verify the final content of the file:
<execute_ipython>
open_file('bad.txt')
@@ -1,8 +0,0 @@
The typos in the bad.txt file have been successfully fixed. The file now contains correct spelling and more appropriate language. Here's a summary of the changes made:
1. Changed "stupid typoo" to "silly typo"
2. Changed "mor" to "more"
The task is complete, and the file has been updated with the corrections.
<finish></finish>
@@ -1,408 +0,0 @@
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
ENVIRONMENT REMINDER: You have 19 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,426 +0,0 @@
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
----------
Certainly! I'll use Jupyter IPython to create a text file containing 'hello world' in the specified location.
<execute_ipython>
create_file('/workspace/test.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/test.txt (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File /workspace/test.txt created.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 18 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,444 +0,0 @@
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
If a bash command returns exit code `-1`, this means the process is not yet finished.
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
or it can contain the text `ctrl+c` to interrupt the process.
For commands that may run indefinitely, the output should be redirected to a file and the command run
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
----------
Certainly! I'll use Jupyter IPython to create a text file containing 'hello world' in the specified location.
<execute_ipython>
create_file('/workspace/test.txt')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/test.txt (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File /workspace/test.txt created.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Now that the file has been created, I'll append the content 'hello world' to it.
<execute_ipython>
append_file('/workspace/test.txt', 'hello world')
</execute_ipython>
----------
OBSERVATION:
[File: /workspace/test.txt (1 lines total after edit)]
(this is the beginning of the file)
1|hello world
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 17 turns left to complete the task. When finished reply with <finish></finish>.
@@ -1,4 +0,0 @@
Certainly! I'll use Jupyter IPython to create a text file containing 'hello world' in the specified location.
<execute_ipython>
create_file('/workspace/test.txt')
@@ -1,4 +0,0 @@
Now that the file has been created, I'll append the content 'hello world' to it.
<execute_ipython>
append_file('/workspace/test.txt', 'hello world')
@@ -1,3 +0,0 @@
The task has been completed successfully. The file '/workspace/test.txt' has been created and contains the text 'hello world'.
<finish></finish>
@@ -1,406 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
The assistant is not allowed to run interactive commands. For commands that may run indefinitely,
the output should be redirected to a file and the command run in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
ENVIRONMENT REMINDER: You have 19 turns left to complete the task. When finished reply with <finish></finish>.
=======
@@ -1,435 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
The assistant is not allowed to run interactive commands. For commands that may run indefinitely,
the output should be redirected to a file and the command run in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
Sure! Let's start by installing the `pymsgbox` package.
<execute_ipython>
%pip install pymsgbox==1.0.9
</execute_ipython>
----------
OBSERVATION:
Collecting pymsgbox==1.0.9
Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
Installing build dependencies ... [?25l- \ | / - done
[?25h Getting requirements to build wheel ... [?25l- done
[?25h Preparing metadata (pyproject.toml) ... [?25l- done
[?25hBuilding wheels for collected packages: pymsgbox
Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=050a965e8c11393aa18fbf84bc66572bd2adbb317662cac9810aba231044ec1b
Stored in directory: /home/openhands/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
Successfully built pymsgbox
Installing collected packages: pymsgbox
Successfully installed pymsgbox-1.0.9
[notice] A new release of pip is available: 24.1 -> 24.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 18 turns left to complete the task. When finished reply with <finish></finish>.
----------
@@ -1,454 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
The assistant is not allowed to run interactive commands. For commands that may run indefinitely,
the output should be redirected to a file and the command run in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
Sure! Let's start by installing the `pymsgbox` package.
<execute_ipython>
%pip install pymsgbox==1.0.9
</execute_ipython>
----------
OBSERVATION:
Collecting pymsgbox==1.0.9
Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
Installing build dependencies ... [?25l- \ | / - done
[?25h Getting requirements to build wheel ... [?25l- done
[?25h Preparing metadata (pyproject.toml) ... [?25l- done
[?25hBuilding wheels for collected packages: pymsgbox
Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=050a965e8c11393aa18fbf84bc66572bd2adbb317662cac9810aba231044ec1b
Stored in directory: /home/openhands/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
Successfully built pymsgbox
Installing collected packages: pymsgbox
Successfully installed pymsgbox-1.0.9
[notice] A new release of pip is available: 24.1 -> 24.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
<execute_ipython>
import pymsgbox
version = pymsgbox.__version__
with open('/workspace/test.txt', 'w') as file:
file.write(f"pymsgbox version: {version}\n")
</execute_ipython>
----------
OBSERVATION:
[Code executed successfully with no output]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
ENVIRONMENT REMINDER: You have 17 turns left to complete the task. When finished reply with <finish></finish>.
----------
@@ -1,471 +0,0 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
The assistant can use a Python environment with <execute_ipython>, e.g.:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
The assistant is not allowed to run interactive commands. For commands that may run indefinitely,
the output should be redirected to a file and the command run in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
If a command execution result says "Command timed out. Sending SIGINT to the process",
the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
If line_number is provided, the window will be moved to include that line.
It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
Args:
path: str: The path to the file to open, preferred absolute path.
line_number: int | None = 1: The line number to move to. Defaults to 1.
context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
goto_line(line_number: int) -> None:
Moves the window to show the specified line number.
Args:
line_number: int: The line number to move to.
scroll_down() -> None:
Moves the window down by 100 lines.
Args:
None
scroll_up() -> None:
Moves the window up by 100 lines.
Args:
None
create_file(filename: str) -> None:
Creates and opens a new file with the given name.
Args:
filename: str: The name of the file to create.
edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
Edit an existing file. This will search for non-empty `to_replace` in the given file and replace it with non-empty `new_content`.
`to_replace` and `new_content` must be different! Split large edits into multiple smaller edits if necessary!
Use `append_file` method for writing after `create_file`!
Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
For example, given a file "/workspace/example.txt" with the following content:
```
line 1
line 2
line 2
line 3
```
EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='new line
line 3',
)
This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
The resulting file will be:
```
line 1
line 2
new line
line 3
```
REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
edit_file_by_replace(
'/workspace/example.txt',
to_replace='line 2
line 3',
new_content='',
)
Args:
file_name: str: The name of the file to edit.
to_replace: str: The content to search for and replace.
new_content: str: The new content to replace the old content with.
insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
Insert content at the given line number in a file.
This will NOT modify the content of the lines before OR after the given line number.
For example, if the file has the following content:
```
line 1
line 2
line 3
```
and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
```
line 1
new line
line 2
line 3
```
Args:
file_name: str: The name of the file to edit.
line_number: int: The line number (starting from 1) to insert the content after.
content: str: The content to insert.
append_file(file_name: str, content: str) -> None:
Append content to the given file.
It appends text `content` to the end of the specified file, ideal after a `create_file`!
Args:
file_name: str: The name of the file to edit.
content: str: The content to insert.
search_dir(search_term: str, dir_path: str = './') -> None:
Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Args:
search_term: str: The term to search for.
dir_path: str: The path to the directory to search.
search_file(search_term: str, file_path: str | None = None) -> None:
Searches for search_term in file. If file is not provided, searches in the current open file.
Args:
search_term: str: The term to search for.
file_path: str | None: The path to the file to search.
find_file(file_name: str, dir_path: str = './') -> None:
Finds all files with the given name in the specified directory.
Args:
file_name: str: The name of the file to find.
dir_path: str: The path to the directory to search.
parse_pdf(file_path: str) -> None:
Parses the content of a PDF file and prints it.
Args:
file_path: str: The path to the file to open.
parse_docx(file_path: str) -> None:
Parses the content of a DOCX file and prints it.
Args:
file_path: str: The path to the file to open.
parse_latex(file_path: str) -> None:
Parses the content of a LaTex file and prints it.
Args:
file_path: str: The path to the file to open.
parse_pptx(file_path: str) -> None:
Parses the content of a pptx file and prints it.
Args:
file_path: str: The path to the file to open.
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
The assistant must avoid apologies and thanks in its responses.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
OBSERVATION:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
OBSERVATION:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
Get the content on "http://127.0.0.1:5000"
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
OBSERVATION:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_ipython>
open_file('app.py')
</execute_ipython>
USER:
[File: /workspace/app.py (10 lines total)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
--- END OF EXAMPLE ---
NOW, LET'S START!Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
Sure! Let's start by installing the `pymsgbox` package.
<execute_ipython>
%pip install pymsgbox==1.0.9
</execute_ipython>
----------
OBSERVATION:
Collecting pymsgbox==1.0.9
Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
Installing build dependencies ... [?25l- \ | / - done
[?25h Getting requirements to build wheel ... [?25l- done
[?25h Preparing metadata (pyproject.toml) ... [?25l- done
[?25hBuilding wheels for collected packages: pymsgbox
Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=050a965e8c11393aa18fbf84bc66572bd2adbb317662cac9810aba231044ec1b
Stored in directory: /home/openhands/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
Successfully built pymsgbox
Installing collected packages: pymsgbox
Successfully installed pymsgbox-1.0.9
[notice] A new release of pip is available: 24.1 -> 24.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
<execute_ipython>
import pymsgbox
version = pymsgbox.__version__
with open('/workspace/test.txt', 'w') as file:
file.write(f"pymsgbox version: {version}\n")
</execute_ipython>
----------
OBSERVATION:
[Code executed successfully with no output]
[Jupyter current working directory: /workspace]
[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
----------
Let's verify the content of `/workspace/test.txt` to ensure the version information was written correctly.
<execute_bash>
cat /workspace/test.txt
</execute_bash>
----------
OBSERVATION:
pymsgbox version: 1.0.9
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
ENVIRONMENT REMINDER: You have 16 turns left to complete the task. When finished reply with <finish></finish>.
----------
@@ -1,4 +0,0 @@
Sure! Let's start by installing the `pymsgbox` package.
<execute_ipython>
%pip install pymsgbox==1.0.9
@@ -1,9 +0,0 @@
Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
<execute_ipython>
import pymsgbox
version = pymsgbox.__version__
with open('/workspace/test.txt', 'w') as file:
file.write(f"pymsgbox version: {version}\n")
@@ -1,4 +0,0 @@
Let's verify the content of `/workspace/test.txt` to ensure the version information was written correctly.
<execute_bash>
cat /workspace/test.txt

Some files were not shown because too many files have changed in this diff Show More