From 606928c9ab1671676791835df756fa6514221c48 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Sun, 17 Nov 2024 23:12:28 +0800 Subject: [PATCH 01/18] add tool param --- .../codeact_agent/function_calling.py | 9 ++++++-- poetry.lock | 23 +++++++++++-------- pyproject.toml | 4 +++- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 42ce1f98db87..5359fa9d9221 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -214,6 +214,8 @@ def __init__(self): * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * The `undo_edit` command will revert the last edit made to the file at `path` +* The `jump_to_definition` command will print out possible FULL definition(s) of the class or function/method in the whole codebase. Can be useful for navigating large repositories +* The `find_references` command will print out possible references (or usages) to the class or function/method in the whole codebase. This can be useful to understand how a class or function/method is used in a repository Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! @@ -235,7 +237,7 @@ def __init__(self): 'type': 'string', }, 'path': { - 'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.', + 'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`. Required for all commands, except `jump_to_definition` and `find_references`.', 'type': 'string', }, 'file_text': { @@ -259,8 +261,11 @@ def __init__(self): 'items': {'type': 'integer'}, 'type': 'array', }, + 'symbol_name': { + 'description': 'Required parameter of `jump_to_definition` and `find_references` commands. The name of the class or function/method to search for.', + }, }, - 'required': ['command', 'path'], + 'required': ['command'], }, ), ) diff --git a/poetry.lock b/poetry.lock index 70d888cafed6..da01d186305e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aenum" @@ -5629,6 +5629,7 @@ optional = false python-versions = ">=3.6" files = [ {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, @@ -5644,14 +5645,12 @@ name = "openhands-aci" version = "0.1.1" description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." optional = false -python-versions = "<4.0,>=3.12" -files = [ - {file = "openhands_aci-0.1.1-py3-none-any.whl", hash = "sha256:8831f97b887571005dca0d70a9f6f0a4f9feb35d3d41f499e70d72b5fb68a599"}, - {file = "openhands_aci-0.1.1.tar.gz", hash = "sha256:705b74a12a8f428e64295b5de125f553500f62ef5ab3a5a6284d8fcf638025e6"}, -] +python-versions = "^3.12" +files = [] +develop = false [package.dependencies] -diskcache = ">=5.6.3,<6.0.0" +diskcache = "^5.6.3" flake8 = "*" gitpython = "*" grep-ast = "0.3.3" @@ -5661,7 +5660,13 @@ numpy = "*" pandas = "*" scipy = "*" tree-sitter = "0.21.3" -whatthepatch = ">=1.0.6,<2.0.0" +whatthepatch = "^1.0.6" + +[package.source] +type = "git" +url = "https://github.com/All-Hands-AI/openhands-aci.git" +reference = "ht/jump-commands" +resolved_reference = "2c815404c3d85fcbe91d0739cf0437f641eead33" [[package]] name = "opentelemetry-api" @@ -10212,4 +10217,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b710448cff0788b563f4d7614fca438ab0b9fe19903a061750012c56da95ff37" +content-hash = "42c0a9b951553b809d865bb3a75a14588ab7d0992b432e37e2140160184d936e" diff --git a/pyproject.toml b/pyproject.toml index 1caad8bf9fc7..c3f9e11321b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ opentelemetry-exporter-otlp-proto-grpc = "1.25.0" modal = "^0.64.145" runloop-api-client = "0.7.0" pygithub = "^2.5.0" -openhands-aci = "^0.1.1" +openhands-aci = {git = "https://github.com/All-Hands-AI/openhands-aci.git", rev = "ht/jump-commands"} [tool.poetry.group.llama-index.dependencies] llama-index = "*" @@ -95,6 +95,7 @@ reportlab = "*" [tool.coverage.run] concurrency = ["gevent"] + [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -125,6 +126,7 @@ ignore = ["D1"] [tool.ruff.lint.pydocstyle] convention = "google" + [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*" From ddad83e46e379311f3550f2f2065f03fd0f5322c Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Sun, 17 Nov 2024 23:49:17 +0800 Subject: [PATCH 02/18] add navigation example --- openhands/llm/fn_call_converter.py | 137 ++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/openhands/llm/fn_call_converter.py b/openhands/llm/fn_call_converter.py index 5ddbcb305064..39dc741c5e47 100644 --- a/openhands/llm/fn_call_converter.py +++ b/openhands/llm/fn_call_converter.py @@ -50,7 +50,7 @@ IN_CONTEXT_LEARNING_EXAMPLE_PREFIX = """ Here's a running example of how to perform a task with the provided tools. ---------------------- START OF EXAMPLE --------------------- +--------------------- START OF EXAMPLE 1 --------------------- USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000. @@ -213,7 +213,140 @@ def index(): ---------------------- END OF EXAMPLE --------------------- +--------------------- END OF EXAMPLE 1 --------------------- + + +--------------------- START OF EXAMPLE 2 --------------------- + +USER: I'm working on an issue about HTTP timeouts in our data ingestion pipeline. The logs show +TimeoutError is being raised in fetch_customer_data() but I'm not sure if we need to increase the +timeout or if there's a deeper issue. Can you help me trace this function? + +ASSISTANT: I'll help track down the timeout issue in fetch_customer_data. Let me see its definition first: + + +jump_to_definition +fetch_customer_data + + +USER: EXECUTION RESULT of [str_replace_editor]: +Found definition in /workspace/services/data_ingestion.py: +async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: + '''Fetches customer data from multiple endpoints and aggregates results. + + Args: + customer_id: Unique identifier for customer + + Returns: + Combined customer data from all sources + ''' + async with CustomerClient(timeout=DEFAULT_TIMEOUT) as client: + # Core profile data has 30s timeout + profile = await client.get_profile(customer_id) + + # Transaction history needs more time + transactions = await _fetch_transactions( + customer_id, + timeout=TRANSACTION_TIMEOUT + ) + + return { + "profile": profile, + "transactions": transactions, + "last_updated": datetime.now() + } + +ASSISTANT: Interesting - it looks like fetch_customer_data relies on _fetch_transactions which has a separate timeout. Let me check the definition of that internal function: + + +jump_to_definition +_fetch_transactions + + +USER: EXECUTION RESULT of [str_replace_editor]: +Found definition in /workspace/services/data_ingestion.py: +async def _fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: + '''Fetches detailed transaction history for customer. + Handles pagination and retries on network errors. + ''' + results = [] + page = 1 + + while True: + try: + async with TransactionClient(timeout=timeout) as client: + batch = await client.get_transactions( + customer_id, + page=page, + batch_size=BATCH_SIZE + ) + if not batch: + break + + results.extend(batch) + page += 1 + + except TimeoutError: + logger.error( + f"Timeout fetching transactions for {customer_id} " + f"on page {page}" + ) + raise + + return results + +ASSISTANT: Now, let me see where fetch_customer_data is being called to understand the usage patterns: + + +find_references +fetch_customer_data + + +USER: EXECUTION RESULT of [str_replace_editor]: +Found references in: +/workspace/api/customers.py: + @router.get("/customers/{customer_id}/full") + async def get_full_customer_data(customer_id: str): + try: + data = await fetch_customer_data(customer_id) + return data + except TimeoutError: + raise HTTPException(status_code=504, detail="Data fetch timed out") + +/workspace/jobs/daily_report.py: + async def generate_customer_report(customer_ids: List[str]): + results = [] + for id in customer_ids: + try: + data = await fetch_customer_data(id) + results.append(summarize_customer_data(data)) + except TimeoutError: + logger.warning(f"Skipping customer {id} due to timeout") + continue + return results + +/workspace/tests/test_services.py: + @pytest.mark.asyncio + async def test_fetch_customer_data_large_history(): + # Test with customer having 50+ pages of transactions + data = await fetch_customer_data("customer_with_large_history") + assert len(data["transactions"]) > 500 + +ASSISTANT: +I see the issue now - the TimeoutError is likely happening because _fetch_transactions has to paginate through transaction history, and while each individual request uses TRANSACTION_TIMEOUT, we're not limiting the total time for all pages. + +Some customers might have hundreds of transaction pages, causing the overall fetch_customer_data call to take much longer than expected. This explains why we see timeouts in both the API endpoint and daily report job. + +We should probably: +1. Add a max_pages parameter to _fetch_transactions to limit total pagination +2. Add a total_timeout parameter separate from the per-request timeout +3. Consider implementing parallel page fetching for large transaction histories + +Would you like me to implement any of these solutions for the issue? + +... (continue the conversation) + +--------------------- END OF EXAMPLE 2 --------------------- Do NOT assume the environment is the same as in the example above. From cee3c263032f5cbf6f814aab4218ff96b88bb219 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Mon, 18 Nov 2024 00:11:56 +0800 Subject: [PATCH 03/18] add line number --- openhands/llm/fn_call_converter.py | 168 +++++++++++++++-------------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/openhands/llm/fn_call_converter.py b/openhands/llm/fn_call_converter.py index 39dc741c5e47..19c0e06f39f5 100644 --- a/openhands/llm/fn_call_converter.py +++ b/openhands/llm/fn_call_converter.py @@ -230,31 +230,33 @@ def index(): USER: EXECUTION RESULT of [str_replace_editor]: -Found definition in /workspace/services/data_ingestion.py: -async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: - '''Fetches customer data from multiple endpoints and aggregates results. - - Args: - customer_id: Unique identifier for customer - - Returns: - Combined customer data from all sources - ''' - async with CustomerClient(timeout=DEFAULT_TIMEOUT) as client: - # Core profile data has 30s timeout - profile = await client.get_profile(customer_id) - - # Transaction history needs more time - transactions = await _fetch_transactions( - customer_id, - timeout=TRANSACTION_TIMEOUT - ) - - return { - "profile": profile, - "transactions": transactions, - "last_updated": datetime.now() - } +Definition(s) of `fetch_customer_data`: +/workspace/services/data_ingestion.py: +...⋮... +252│async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: +253│ '''Fetches customer data from multiple endpoints and aggregates results. +254│ +255│ Args: +256│ customer_id: Unique identifier for customer +257│ +258│ Returns: +259│ Combined customer data from all sources +260│ ''' +261│ async with CustomerClient(timeout=DEFAULT_TIMEOUT) as client: +262│ # Core profile data has 30s timeout +263│ profile = await client.get_profile(customer_id) +264│ +265│ # Transaction history needs more time +266│ transactions = await _fetch_transactions( +267│ customer_id, +268│ timeout=TRANSACTION_TIMEOUT +269│ ) +270│ +271│ return { +272│ "profile": profile, +273│ "transactions": transactions, +274│ "last_updated": datetime.now() +275│ } ASSISTANT: Interesting - it looks like fetch_customer_data relies on _fetch_transactions which has a separate timeout. Let me check the definition of that internal function: @@ -264,36 +266,38 @@ async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: USER: EXECUTION RESULT of [str_replace_editor]: -Found definition in /workspace/services/data_ingestion.py: -async def _fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: - '''Fetches detailed transaction history for customer. - Handles pagination and retries on network errors. - ''' - results = [] - page = 1 - - while True: - try: - async with TransactionClient(timeout=timeout) as client: - batch = await client.get_transactions( - customer_id, - page=page, - batch_size=BATCH_SIZE - ) - if not batch: - break - - results.extend(batch) - page += 1 - - except TimeoutError: - logger.error( - f"Timeout fetching transactions for {customer_id} " - f"on page {page}" - ) - raise - - return results +Definition(s) of `_fetch_transactions`: +/workspace/services/data_ingestion.py: +...⋮... +67|async def _fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: +68| '''Fetches detailed transaction history for customer. +69| Handles pagination and retries on network errors. +70| ''' +71| results = [] +72| page = 1 +73| +74| while True: +75| try: +76| async with TransactionClient(timeout=timeout) as client: +77| batch = await client.get_transactions( +78| customer_id, +79| page=page, +80| batch_size=BATCH_SIZE +81| ) +82| if not batch: +83| break +84| +85| results.extend(batch) +86| page += 1 +87| +88| except TimeoutError: +89| logger.error( +90| f"Timeout fetching transactions for {customer_id} " +91| f"on page {page}" +92| ) +93| raise +94| +95| return results ASSISTANT: Now, let me see where fetch_customer_data is being called to understand the usage patterns: @@ -303,34 +307,38 @@ async def _fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: USER: EXECUTION RESULT of [str_replace_editor]: -Found references in: +References to `fetch_customer_data`: /workspace/api/customers.py: - @router.get("/customers/{customer_id}/full") - async def get_full_customer_data(customer_id: str): - try: - data = await fetch_customer_data(customer_id) - return data - except TimeoutError: - raise HTTPException(status_code=504, detail="Data fetch timed out") - +...⋮... +21| @router.get("/customers/{customer_id}/full") +22| async def get_full_customer_data(customer_id: str): +23| try: +24| data = await fetch_customer_data(customer_id) +25| return data +26| except TimeoutError: +27| raise HTTPException(status_code=504, detail="Data fetch timed out") +...⋮... /workspace/jobs/daily_report.py: - async def generate_customer_report(customer_ids: List[str]): - results = [] - for id in customer_ids: - try: - data = await fetch_customer_data(id) - results.append(summarize_customer_data(data)) - except TimeoutError: - logger.warning(f"Skipping customer {id} due to timeout") - continue - return results - +...⋮... +314| async def generate_customer_report(customer_ids: List[str]): +315| results = [] +316| for id in customer_ids: +317| try: +318| data = await fetch_customer_data(id) +319| results.append(summarize_customer_data(data)) +320| except TimeoutError: +321| logger.warning(f"Skipping customer {id} due to timeout") +322| continue +323| return results +...⋮... /workspace/tests/test_services.py: - @pytest.mark.asyncio - async def test_fetch_customer_data_large_history(): - # Test with customer having 50+ pages of transactions - data = await fetch_customer_data("customer_with_large_history") - assert len(data["transactions"]) > 500 +...⋮... +60| @pytest.mark.asyncio +61| async def test_fetch_customer_data_large_history(): +62| # Test with customer having 50+ pages of transactions +63| data = await fetch_customer_data("customer_with_large_history") +64| assert len(data["transactions"]) > 500 +...⋮... ASSISTANT: I see the issue now - the TimeoutError is likely happening because _fetch_transactions has to paginate through transaction history, and while each individual request uses TRANSACTION_TIMEOUT, we're not limiting the total time for all pages. From 31c6733ffbe9fce15bb90853fa145032985629cf Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Mon, 18 Nov 2024 17:39:13 +0800 Subject: [PATCH 04/18] fix bug --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index da01d186305e..e0ff616e0a53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5666,7 +5666,7 @@ whatthepatch = "^1.0.6" type = "git" url = "https://github.com/All-Hands-AI/openhands-aci.git" reference = "ht/jump-commands" -resolved_reference = "2c815404c3d85fcbe91d0739cf0437f641eead33" +resolved_reference = "f7808974f1abeabfb5c98a36c52c020361dc9125" [[package]] name = "opentelemetry-api" From 3a1229c7ebb4101bda6c11d816c98844dd464712 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 20 Nov 2024 08:18:21 +0000 Subject: [PATCH 05/18] fix bug --- openhands/agenthub/codeact_agent/function_calling.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 5359fa9d9221..70d7adac104b 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -232,8 +232,16 @@ def __init__(self): 'type': 'object', 'properties': { 'command': { - 'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.', - 'enum': ['view', 'create', 'str_replace', 'insert', 'undo_edit'], + 'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`, `jump_to_definition`, `find_references`.', + 'enum': [ + 'view', + 'create', + 'str_replace', + 'insert', + 'undo_edit', + 'jump_to_definition', + 'find_references', + ], 'type': 'string', }, 'path': { From 32e0c1d4ddcec867fdc2615874ef19b4e226c0df Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Thu, 21 Nov 2024 07:42:00 +0000 Subject: [PATCH 06/18] update editor tool desc --- .../codeact_agent/function_calling.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 70d7adac104b..3f5f8df3803d 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -208,19 +208,25 @@ def __init__(self): ), ) -_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files -* State is persistent across command calls and discussions with the user -* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep -* The `create` command cannot be used if the specified `path` already exists as a file -* If a `command` generates a long output, it will be truncated and marked with `` -* The `undo_edit` command will revert the last edit made to the file at `path` -* The `jump_to_definition` command will print out possible FULL definition(s) of the class or function/method in the whole codebase. Can be useful for navigating large repositories -* The `find_references` command will print out possible references (or usages) to the class or function/method in the whole codebase. This can be useful to understand how a class or function/method is used in a repository - -Notes for using the `str_replace` command: -* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! -* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique -* The `new_str` parameter should contain the edited lines that should replace the `old_str` +_STR_REPLACE_EDITOR_DESCRIPTION = """Custom file editing tool with persistent state across interactions. +Key Commands: +* `view`: + * For files: displays content with line numbers similar to `cat -n` + * For directories: lists non-hidden files/directories up to 2 levels deep +* `create`: cannot create file if path already exists +* `str_replace`: replaces text in a file +* `undo_edit`: reverts last edit +* `jump_to_definition`: finds full definition of a class/function/method +* `find_references`: locates usages of a class/function/method + +`str_replace` Command Notes: +* `old_str` must match EXACTLY one or more consecutive lines +* `old_str` must be unique in the file +* Include sufficient context to ensure uniqueness +* Be careful with whitespaces + +Output Characteristics: +* Long outputs are truncated and marked with """ StrReplaceEditorTool = ChatCompletionToolParam( From b92fa894896c627e2283b6a535351ac8429aa624 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Thu, 21 Nov 2024 09:22:40 +0000 Subject: [PATCH 07/18] update in-context example --- openhands/llm/fn_call_converter.py | 182 +++++++++++++++++------------ 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/openhands/llm/fn_call_converter.py b/openhands/llm/fn_call_converter.py index 19c0e06f39f5..cbaf1a0faa66 100644 --- a/openhands/llm/fn_call_converter.py +++ b/openhands/llm/fn_call_converter.py @@ -218,88 +218,116 @@ def index(): --------------------- START OF EXAMPLE 2 --------------------- -USER: I'm working on an issue about HTTP timeouts in our data ingestion pipeline. The logs show -TimeoutError is being raised in fetch_customer_data() but I'm not sure if we need to increase the -timeout or if there's a deeper issue. Can you help me trace this function? +USER: I'm seeing timeouts in our data ingestion pipeline (issue #127). The logs show it's happening in our customer data fetch but I'm not sure where to start looking. Can you help me investigate? -ASSISTANT: I'll help track down the timeout issue in fetch_customer_data. Let me see its definition first: +ASSISTANT: Let's first look at the structure of our workspace to locate relevant files: -jump_to_definition -fetch_customer_data +view +/workspace USER: EXECUTION RESULT of [str_replace_editor]: -Definition(s) of `fetch_customer_data`: -/workspace/services/data_ingestion.py: -...⋮... -252│async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: -253│ '''Fetches customer data from multiple endpoints and aggregates results. -254│ -255│ Args: -256│ customer_id: Unique identifier for customer -257│ -258│ Returns: -259│ Combined customer data from all sources -260│ ''' -261│ async with CustomerClient(timeout=DEFAULT_TIMEOUT) as client: -262│ # Core profile data has 30s timeout -263│ profile = await client.get_profile(customer_id) -264│ -265│ # Transaction history needs more time -266│ transactions = await _fetch_transactions( -267│ customer_id, -268│ timeout=TRANSACTION_TIMEOUT -269│ ) -270│ -271│ return { -272│ "profile": profile, -273│ "transactions": transactions, -274│ "last_updated": datetime.now() -275│ } - -ASSISTANT: Interesting - it looks like fetch_customer_data relies on _fetch_transactions which has a separate timeout. Let me check the definition of that internal function: +Here's the files and directories up to 2 levels deep in /workspace, excluding hidden items: +/workspace +/workspace/jobs +/workspace/jobs/__init__.py +/workspace/jobs/daily_report.py +/workspace/utils +/workspace/utils/__init__.py +/workspace/utils/logging.py +/workspace/api +/workspace/api/routes.py +/workspace/api/customers.py +/workspace/services +/workspace/services/__init__.py +/workspace/services/clients.py +/workspace/services/config.py +/workspace/services/data_ingestion.py +/workspace/services/helpers.py +/workspace/tests +/workspace/tests/test_services.py + +ASSISTANT: I see a `services/data_ingestion.py` file which is likely relevant to our data ingestion +pipeline. Let's take a look at it: + + +view +/workspace/services/data_ingestion.py + + +USER: EXECUTION RESULT of [str_replace_editor]: +Here's the result of running `cat -n` on /workspace/services/data_ingestion.py: + 1 from typing import Dict, Any, List + 2 from datetime import datetime + 3 import logging + 4 + 5 from .clients import CustomerClient, TransactionClient + 6 from .config import DEFAULT_TIMEOUT, TRANSACTION_TIMEOUT, BATCH_SIZE + 7 + 8 logger = logging.getLogger(__name__) + 9 + 10 async def fetch_customer_data(customer_id: str) -> Dict[str, Any]: + 11 '''Fetches customer data from multiple endpoints and aggregates results.''' + 12 async with CustomerClient(timeout=DEFAULT_TIMEOUT) as client: + 13 profile = await client.get_profile(customer_id) + 14 + 15 transactions = await fetch_transactions( + 16 customer_id, + 17 timeout=TRANSACTION_TIMEOUT + 18 ) + 19 + 20 return { + 21 "profile": profile, + 22 "transactions": transactions, + 24 "last_updated": datetime.now() + 25 } + 26 +# More functions below... + +ASSISTANT: I see a fetch_customer_data function that seems to be the entry point. It's calling some fetch_transactions function with a specific timeout. Let's see its definition: jump_to_definition -_fetch_transactions +fetch_transactions USER: EXECUTION RESULT of [str_replace_editor]: -Definition(s) of `_fetch_transactions`: -/workspace/services/data_ingestion.py: +Definition(s) of `fetch_transactions`: +/workspace/services/helpers.py: +...⋮... +252|async def fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: +253| '''Fetches detailed transaction history for customer. +254| Handles pagination and retries on network errors. +255| ''' +256| results = [] +257| page = 1 +258| +259| while True: +260| try: +261| async with TransactionClient(timeout=timeout) as client: +263| batch = await client.get_transactions( +264| customer_id, +265| page=page, +266| batch_size=BATCH_SIZE +267| ) +268| if not batch: +269| break +270| +271| results.extend(batch) +272| page += 1 +273| +274| except TimeoutError: +275| logger.error( +276| f"Timeout fetching transactions for {customer_id} " +277| f"on page {page}" +278| ) +279| raise +280| +281| return results ...⋮... -67|async def _fetch_transactions(customer_id: str, timeout: int) -> List[Dict]: -68| '''Fetches detailed transaction history for customer. -69| Handles pagination and retries on network errors. -70| ''' -71| results = [] -72| page = 1 -73| -74| while True: -75| try: -76| async with TransactionClient(timeout=timeout) as client: -77| batch = await client.get_transactions( -78| customer_id, -79| page=page, -80| batch_size=BATCH_SIZE -81| ) -82| if not batch: -83| break -84| -85| results.extend(batch) -86| page += 1 -87| -88| except TimeoutError: -89| logger.error( -90| f"Timeout fetching transactions for {customer_id} " -91| f"on page {page}" -92| ) -93| raise -94| -95| return results - -ASSISTANT: Now, let me see where fetch_customer_data is being called to understand the usage patterns: + +ASSISTANT: Interesting, this function is paginating through transactions but using the same timeout for each page. Let's see where fetch_customer_data is being used to understand the impact: find_references @@ -341,16 +369,18 @@ def index(): ...⋮... ASSISTANT: -I see the issue now - the TimeoutError is likely happening because _fetch_transactions has to paginate through transaction history, and while each individual request uses TRANSACTION_TIMEOUT, we're not limiting the total time for all pages. - -Some customers might have hundreds of transaction pages, causing the overall fetch_customer_data call to take much longer than expected. This explains why we see timeouts in both the API endpoint and daily report job. - -We should probably: -1. Add a max_pages parameter to _fetch_transactions to limit total pagination +Now I understand the full picture. The timeout is happening because: +1. The API endpoint and daily report job both call fetch_customer_data +2. This function uses fetch_transactions which paginates through ALL transaction history +3. While each page request has a timeout, there's no limit on total pages or total time +4. Our tests show we expect to handle 50+ pages of transactions + +To fix this, we should: +1. Add a max_pages parameter to fetch_transactions to limit total pagination 2. Add a total_timeout parameter separate from the per-request timeout 3. Consider implementing parallel page fetching for large transaction histories -Would you like me to implement any of these solutions for the issue? +Would you like me to help implement any of these solutions? ... (continue the conversation) From e5decf24c76d5763918dd6cc84fe86b01a1f1c6d Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Thu, 21 Nov 2024 12:00:17 +0000 Subject: [PATCH 08/18] use minimal change for tool desc --- .../codeact_agent/function_calling.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 3f5f8df3803d..0b5d3f66a02a 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -208,25 +208,17 @@ def __init__(self): ), ) -_STR_REPLACE_EDITOR_DESCRIPTION = """Custom file editing tool with persistent state across interactions. -Key Commands: -* `view`: - * For files: displays content with line numbers similar to `cat -n` - * For directories: lists non-hidden files/directories up to 2 levels deep -* `create`: cannot create file if path already exists -* `str_replace`: replaces text in a file -* `undo_edit`: reverts last edit -* `jump_to_definition`: finds full definition of a class/function/method -* `find_references`: locates usages of a class/function/method - -`str_replace` Command Notes: -* `old_str` must match EXACTLY one or more consecutive lines -* `old_str` must be unique in the file -* Include sufficient context to ensure uniqueness -* Be careful with whitespaces - -Output Characteristics: -* Long outputs are truncated and marked with +_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files, with persistent state across interactions +* If `path` is a file, `view` displays the result of applying `cat -n`. Otherwise `view` lists non-hidden files and directories up to 2 levels deep +* The `create` command cannot be used if the specified `path` already exists as a file +* The command output will be truncated and marked with `` if it's too long +* The `undo_edit` command will revert the last edit made to the file +* Use `jump_to_definition` and `find_references` to quickly navigate through large codebases + +Notes for using the `str_replace` command: +* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! +* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique +* The `new_str` parameter should contain the edited lines that should replace the `old_str` """ StrReplaceEditorTool = ChatCompletionToolParam( From 390b9cbcf6f798f647efdd9aa7d06642b6046f59 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Thu, 21 Nov 2024 12:36:40 +0000 Subject: [PATCH 09/18] fix runtime tests & clean --- poetry.lock | 103 ++++++++++++++++++++++++++++++++- tests/unit/test_agent_skill.py | 84 --------------------------- 2 files changed, 102 insertions(+), 85 deletions(-) diff --git a/poetry.lock b/poetry.lock index e0ff616e0a53..e524bffb6d3f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5658,6 +5658,7 @@ litellm = "*" networkx = "*" numpy = "*" pandas = "*" +rapidfuzz = "*" scipy = "*" tree-sitter = "0.21.3" whatthepatch = "^1.0.6" @@ -5666,7 +5667,7 @@ whatthepatch = "^1.0.6" type = "git" url = "https://github.com/All-Hands-AI/openhands-aci.git" reference = "ht/jump-commands" -resolved_reference = "f7808974f1abeabfb5c98a36c52c020361dc9125" +resolved_reference = "08ca8cc85a161d2c271451bd49ba35251e41889d" [[package]] name = "opentelemetry-api" @@ -7450,6 +7451,106 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "rapidfuzz" +version = "3.10.1" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.9" +files = [ + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f17d9f21bf2f2f785d74f7b0d407805468b4c173fa3e52c86ec94436b338e74a"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b31f358a70efc143909fb3d75ac6cd3c139cd41339aa8f2a3a0ead8315731f2b"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4f43f2204b56a61448ec2dd061e26fd344c404da99fb19f3458200c5874ba2"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d81bf186a453a2757472133b24915768abc7c3964194406ed93e170e16c21cb"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3611c8f45379a12063d70075c75134f2a8bd2e4e9b8a7995112ddae95ca1c982"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c3b537b97ac30da4b73930fa8a4fe2f79c6d1c10ad535c5c09726612cd6bed9"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231ef1ec9cf7b59809ce3301006500b9d564ddb324635f4ea8f16b3e2a1780da"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed4f3adc1294834955b7e74edd3c6bd1aad5831c007f2d91ea839e76461a5879"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b6015da2e707bf632a71772a2dbf0703cff6525732c005ad24987fe86e8ec32"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b35a118d61d6f008e8e3fb3a77674d10806a8972c7b8be433d6598df4d60b01"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc308d79a7e877226f36bdf4e149e3ed398d8277c140be5c1fd892ec41739e6d"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f017dbfecc172e2d0c37cf9e3d519179d71a7f16094b57430dffc496a098aa17"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win32.whl", hash = "sha256:36c0e1483e21f918d0f2f26799fe5ac91c7b0c34220b73007301c4f831a9c4c7"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:10746c1d4c8cd8881c28a87fd7ba0c9c102346dfe7ff1b0d021cdf093e9adbff"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_arm64.whl", hash = "sha256:dfa64b89dcb906835e275187569e51aa9d546a444489e97aaf2cc84011565fbe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:779027d3307e1a2b1dc0c03c34df87a470a368a1a0840a9d2908baf2d4067956"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:440b5608ab12650d0390128d6858bc839ae77ffe5edf0b33a1551f2fa9860651"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cac41a411e07a6f3dc80dfbd33f6be70ea0abd72e99c59310819d09f07d945"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:958473c9f0bca250590200fd520b75be0dbdbc4a7327dc87a55b6d7dc8d68552"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ef60dfa73749ef91cb6073be1a3e135f4846ec809cc115f3cbfc6fe283a5584"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fbac18f2c19fc983838a60611e67e3262e36859994c26f2ee85bb268de2355"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0d519ff39db887cd73f4e297922786d548f5c05d6b51f4e6754f452a7f4296"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bebb7bc6aeb91cc57e4881b222484c26759ca865794187217c9dcea6c33adae6"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe07f8b9c3bb5c5ad1d2c66884253e03800f4189a60eb6acd6119ebaf3eb9894"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfa48a4a2d45a41457f0840c48e579db157a927f4e97acf6e20df8fc521c79de"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cf44d01bfe8ee605b7eaeecbc2b9ca64fc55765f17b304b40ed8995f69d7716"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e6bbca9246d9eedaa1c84e04a7f555493ba324d52ae4d9f3d9ddd1b740dcd87"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win32.whl", hash = "sha256:567f88180f2c1423b4fe3f3ad6e6310fc97b85bdba574801548597287fc07028"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6b2cd7c29d6ecdf0b780deb587198f13213ac01c430ada6913452fd0c40190fc"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_arm64.whl", hash = "sha256:9f912d459e46607ce276128f52bea21ebc3e9a5ccf4cccfef30dd5bddcf47be8"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac4452f182243cfab30ba4668ef2de101effaedc30f9faabb06a095a8c90fd16"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:565c2bd4f7d23c32834652b27b51dd711814ab614b4e12add8476be4e20d1cf5"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d9747149321607be4ccd6f9f366730078bed806178ec3eeb31d05545e9e8f"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:616290fb9a8fa87e48cb0326d26f98d4e29f17c3b762c2d586f2b35c1fd2034b"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:073a5b107e17ebd264198b78614c0206fa438cce749692af5bc5f8f484883f50"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39c4983e2e2ccb9732f3ac7d81617088822f4a12291d416b09b8a1eadebb3e29"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac7adee6bcf0c6fee495d877edad1540a7e0f5fc208da03ccb64734b43522d7a"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:425f4ac80b22153d391ee3f94bc854668a0c6c129f05cf2eaf5ee74474ddb69e"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a2fa13e8a219f9b5dcb9e74abe3ced5838a7327e629f426d333dfc8c5a6e66"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75561f3df9a906aaa23787e9992b228b1ab69007932dc42070f747103e177ba8"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd062490537e97ca125bc6c7f2b7331c2b73d21dc304615afe61ad1691e15d5"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfcc8feccf63245a22dfdd16e222f1a39771a44b870beb748117a0e09cbb4a62"}, + {file = "rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979"}, +] + +[package.extras] +all = ["numpy"] + [[package]] name = "referencing" version = "0.35.1" diff --git a/tests/unit/test_agent_skill.py b/tests/unit/test_agent_skill.py index 6079eb659aea..63745f4dd2b4 100644 --- a/tests/unit/test_agent_skill.py +++ b/tests/unit/test_agent_skill.py @@ -715,87 +715,3 @@ def test_parse_pptx(tmp_path): 'Hello, this is the second test PPTX slide.\n\n' ) assert output == expected_output, f'Expected output does not match. Got: {output}' - - -# ============================================================================= - - -def test_file_editor_view(tmp_path): - # generate a random directory - random_dir = tmp_path / 'dir_1' - random_dir.mkdir() - # create a file in the directory - random_file = random_dir / 'a.txt' - random_file.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5') - random_dir_2 = tmp_path / 'dir_2' - random_dir_2.mkdir() - random_file_2 = random_dir_2 / 'b.txt' - random_file_2.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5') - - from openhands.runtime.plugins.agent_skills.agentskills import file_editor - - # view the file - result = file_editor(command='view', path=str(random_file)) - print('\n', result) - assert result is not None - assert ( - result.split('\n') - == f"""Here's the result of running `cat -n` on {random_file}: - 1\tLine 1 - 2\tLine 2 - 3\tLine 3 - 4\tLine 4 - 5\tLine 5 -""".split('\n') - ) - - # view the directory - result = file_editor(command='view', path=str(tmp_path)) - print('\n', result) - assert result is not None - assert ( - result.strip().split('\n') - == f"""Here's the files and directories up to 2 levels deep in {tmp_path}, excluding hidden items: -{tmp_path} -{tmp_path}/dir_2 -{tmp_path}/dir_2/b.txt -{tmp_path}/dir_1 -{tmp_path}/dir_1/a.txt -""".strip().split('\n') - ) - - -def test_file_editor_create(tmp_path): - # generate a random directory - random_dir = tmp_path / 'dir_1' - random_dir.mkdir() - # create a file in the directory - random_file = random_dir / 'a.txt' - - from openhands.runtime.plugins.agent_skills.agentskills import file_editor - - # view an unexist file - result = file_editor(command='view', path=str(random_file)) - print(result) - assert result is not None - assert ( - result - == f'ERROR:\nInvalid `path` parameter: {random_file}. The path {random_file} does not exist. Please provide a valid path.' - ) - - # create a file - result = file_editor(command='create', path=str(random_file), file_text='Line 6') - print(result) - assert result is not None - assert result == f'File created successfully at: {random_file}' - - # view again - result = file_editor(command='view', path=str(random_file)) - print(result) - assert result is not None - assert ( - result.strip().split('\n') - == f"""Here's the result of running `cat -n` on {random_file}: - 1\tLine 6 -""".strip().split('\n') - ) From 61f4c2cc2bae066d7c418e7b14586d3da4eeb7dc Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Fri, 22 Nov 2024 16:48:55 +0000 Subject: [PATCH 10/18] fix ci --- containers/app/Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index 266a9d6b3e40..df3c5c4427f0 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -61,6 +61,15 @@ RUN chown -R openhands:app /app && chmod -R 770 /app RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE USER openhands +RUN GIT_PATH=$(command -v git) && \ + if [ -n "$GIT_PATH" ]; then \ + echo "Git found at $GIT_PATH"; \ + GIT_DIR=$(dirname "$GIT_PATH"); \ + export PATH="$GIT_DIR:$PATH"; \ + else \ + echo "Error: Git not found" && exit 1; \ + fi + ENV VIRTUAL_ENV=/app/.venv \ PATH="/app/.venv/bin:$PATH" \ PYTHONPATH='/app' From fb356f1e22f8ca3933c856f9511eb71185843c5a Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Fri, 22 Nov 2024 17:15:41 +0000 Subject: [PATCH 11/18] fix ci --- containers/app/Dockerfile | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index df3c5c4427f0..fea1168b2b13 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -61,18 +61,10 @@ RUN chown -R openhands:app /app && chmod -R 770 /app RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE USER openhands -RUN GIT_PATH=$(command -v git) && \ - if [ -n "$GIT_PATH" ]; then \ - echo "Git found at $GIT_PATH"; \ - GIT_DIR=$(dirname "$GIT_PATH"); \ - export PATH="$GIT_DIR:$PATH"; \ - else \ - echo "Error: Git not found" && exit 1; \ - fi - ENV VIRTUAL_ENV=/app/.venv \ PATH="/app/.venv/bin:$PATH" \ - PYTHONPATH='/app' + PYTHONPATH='/app' \ + PATH="$PATH:$(dirname $(command -v git))" COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} RUN playwright install --with-deps chromium From e0b41853a32f1a4d3fe226c66931028fda24dd24 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Fri, 22 Nov 2024 17:28:41 +0000 Subject: [PATCH 12/18] fix ci --- containers/app/Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index fea1168b2b13..0707089882fd 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -62,9 +62,8 @@ RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_ USER openhands ENV VIRTUAL_ENV=/app/.venv \ - PATH="/app/.venv/bin:$PATH" \ - PYTHONPATH='/app' \ - PATH="$PATH:$(dirname $(command -v git))" + PATH="/app/.venv/bin:$PATH:$(dirname $(command -v git))" \ + PYTHONPATH='/app' COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} RUN playwright install --with-deps chromium From b85a0324a633d5a25aa123f936e27013c20c9d98 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Fri, 22 Nov 2024 17:44:34 +0000 Subject: [PATCH 13/18] update aci --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e524bffb6d3f..f006d9d355a8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5667,7 +5667,7 @@ whatthepatch = "^1.0.6" type = "git" url = "https://github.com/All-Hands-AI/openhands-aci.git" reference = "ht/jump-commands" -resolved_reference = "08ca8cc85a161d2c271451bd49ba35251e41889d" +resolved_reference = "e07a6c329ff3b712c0cb06777e901892f4e0a93d" [[package]] name = "opentelemetry-api" From 701e3b15593c3f8fbe4b045279d4304df0d8a562 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Fri, 22 Nov 2024 17:55:32 +0000 Subject: [PATCH 14/18] revert Dockerfile --- containers/app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index 0707089882fd..266a9d6b3e40 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -62,7 +62,7 @@ RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_ USER openhands ENV VIRTUAL_ENV=/app/.venv \ - PATH="/app/.venv/bin:$PATH:$(dirname $(command -v git))" \ + PATH="/app/.venv/bin:$PATH" \ PYTHONPATH='/app' COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} From 8b3bb2452193d24ef3e869e519810997efb8b287 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 4 Dec 2024 07:11:23 +0000 Subject: [PATCH 15/18] update aci --- poetry.lock | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 16060bb886a1..ec1f64b242c5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5507,8 +5507,8 @@ whatthepatch = "^1.0.6" [package.source] type = "git" url = "https://github.com/All-Hands-AI/openhands-aci.git" -reference = "ht/jump-commands" -resolved_reference = "e07a6c329ff3b712c0cb06777e901892f4e0a93d" +reference = "ht/context-suggestion" +resolved_reference = "6ce6861816470b49c2e7a57c38e1548e8cdfb4d6" [[package]] name = "opentelemetry-api" @@ -10192,4 +10192,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "cff0f5a52b55b7d2e05f39c2cc7f1b8ea3e324a15173051726cd57abb069a192" +content-hash = "5442dbd20d83d3ec951a4d97fd4008f437e0898edc7318fe8fc6e079118c2bb3" diff --git a/pyproject.toml b/pyproject.toml index 253613848279..3bd6ba73427a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ modal = "^0.66.26" runloop-api-client = "0.10.0" pygithub = "^2.5.0" joblib = "*" -openhands-aci = {git = "https://github.com/All-Hands-AI/openhands-aci.git", rev = "ht/jump-commands"} +openhands-aci = {git = "https://github.com/All-Hands-AI/openhands-aci.git", rev = "ht/context-suggestion"} python-socketio = "^5.11.4" redis = "^5.2.0" From b528311325689623869652cae514bffbae729fbb Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 4 Dec 2024 07:28:09 +0000 Subject: [PATCH 16/18] update aci --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ec1f64b242c5..9e122baaee36 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5483,7 +5483,7 @@ numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} [[package]] name = "openhands-aci" -version = "0.1.1" +version = "0.1.2" description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." optional = false python-versions = "^3.12" @@ -5507,8 +5507,8 @@ whatthepatch = "^1.0.6" [package.source] type = "git" url = "https://github.com/All-Hands-AI/openhands-aci.git" -reference = "ht/context-suggestion" -resolved_reference = "6ce6861816470b49c2e7a57c38e1548e8cdfb4d6" +reference = "ht/jump-commands" +resolved_reference = "721f2fc2270f52d72e2b8ed6bc6af9a392241eff" [[package]] name = "opentelemetry-api" @@ -10192,4 +10192,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5442dbd20d83d3ec951a4d97fd4008f437e0898edc7318fe8fc6e079118c2bb3" +content-hash = "cff0f5a52b55b7d2e05f39c2cc7f1b8ea3e324a15173051726cd57abb069a192" diff --git a/pyproject.toml b/pyproject.toml index 3bd6ba73427a..253613848279 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ modal = "^0.66.26" runloop-api-client = "0.10.0" pygithub = "^2.5.0" joblib = "*" -openhands-aci = {git = "https://github.com/All-Hands-AI/openhands-aci.git", rev = "ht/context-suggestion"} +openhands-aci = {git = "https://github.com/All-Hands-AI/openhands-aci.git", rev = "ht/jump-commands"} python-socketio = "^5.11.4" redis = "^5.2.0" From 2e56ce0fe41870de759d06412d969f9520251cd5 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 4 Dec 2024 04:13:35 +0000 Subject: [PATCH 17/18] handle tag --- openhands/runtime/action_execution_server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index b48a9b55a693..9f5aaa5178e0 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -9,8 +9,10 @@ import asyncio import base64 import io +import json import mimetypes import os +import re import shutil import tempfile import time @@ -199,6 +201,12 @@ async def run_ipython(self, action: IPythonRunCellAction) -> Observation: obs: IPythonRunCellObservation = await _jupyter_plugin.run(action) obs.content = obs.content.rstrip() + match = re.search( + r'(.*?)', obs.content, re.DOTALL + ) + if match: + result_dict = json.loads(match.group(1)) + obs.content = result_dict.get('formatted_output_and_error', '') if action.include_extra: obs.content += ( f'\n[Jupyter current working directory: {self.bash_session.pwd}]' From 4c92c89414ae405231a00fc653d1bd9323b76670 Mon Sep 17 00:00:00 2001 From: Hoang Tran Date: Wed, 4 Dec 2024 05:45:53 +0000 Subject: [PATCH 18/18] handle multiple file editor calls --- openhands/runtime/action_execution_server.py | 22 ++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 9f5aaa5178e0..26b728284e33 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -201,12 +201,26 @@ async def run_ipython(self, action: IPythonRunCellAction) -> Observation: obs: IPythonRunCellObservation = await _jupyter_plugin.run(action) obs.content = obs.content.rstrip() - match = re.search( + matches = re.findall( r'(.*?)', obs.content, re.DOTALL ) - if match: - result_dict = json.loads(match.group(1)) - obs.content = result_dict.get('formatted_output_and_error', '') + if matches: + results = [] + for match in matches: + try: + result_dict = json.loads(match) + results.append( + result_dict.get('formatted_output_and_error', '') + ) + except json.JSONDecodeError: + # Handle JSON decoding errors if necessary + results.append( + f"Invalid JSON in 'openhands-aci' output: {match}" + ) + + # Combine the results (e.g., join them) or handle them as required + obs.content = '\n'.join(results) + if action.include_extra: obs.content += ( f'\n[Jupyter current working directory: {self.bash_session.pwd}]'