mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
integrate LocAgent into OpenHands (#7371)
Co-authored-by: czlll <gangda@huaihe.usc.edu> Co-authored-by: Hoang Tran <descience.thh10@gmail.com>
This commit is contained in:
parent
fa5b52298e
commit
efe287ce34
69
evaluation/benchmarks/swe_bench/loc_prompt.py
Normal file
69
evaluation/benchmarks/swe_bench/loc_prompt.py
Normal file
@ -0,0 +1,69 @@
|
||||
TASK_INSTRUECTION="""
|
||||
Given the following GitHub problem description, your objective is to localize the specific files, classes or functions, and lines of code that need modification or contain key information to resolve the issue.
|
||||
|
||||
Follow these steps to localize the issue:
|
||||
## Step 1: Categorize and Extract Key Problem Information
|
||||
- Classify the problem statement into the following categories:
|
||||
Problem description, error trace, code to reproduce the bug, and additional context.
|
||||
- Identify modules in the '{package_name}' package mentioned in each category.
|
||||
- Use extracted keywords and line numbers to search for relevant code references for additional context.
|
||||
|
||||
## Step 2: Locate Referenced Modules
|
||||
- Accurately determine specific modules
|
||||
- Explore the repo to familiarize yourself with its structure.
|
||||
- Analyze the described execution flow to identify specific modules or components being referenced.
|
||||
- Pay special attention to distinguishing between modules with similar names using context and described execution flow.
|
||||
- Output Format for collected relevant modules:
|
||||
- Use the format: 'file_path:QualifiedName'
|
||||
- E.g., for a function `calculate_sum` in the `MathUtils` class located in `src/helpers/math_helpers.py`, represent it as: 'src/helpers/math_helpers.py:MathUtils.calculate_sum'.
|
||||
|
||||
## Step 3: Analyze and Reproducing the Problem
|
||||
- Clarify the Purpose of the Issue
|
||||
- If expanding capabilities: Identify where and how to incorporate new behavior, fields, or modules.
|
||||
- If addressing unexpected behavior: Focus on localizing modules containing potential bugs.
|
||||
- Reconstruct the execution flow
|
||||
- Identify main entry points triggering the issue.
|
||||
- Trace function calls, class interactions, and sequences of events.
|
||||
- Identify potential breakpoints causing the issue.
|
||||
Important: Keep the reconstructed flow focused on the problem, avoiding irrelevant details.
|
||||
|
||||
## Step 4: Locate Areas for Modification
|
||||
- Locate specific files, functions, or lines of code requiring changes or containing critical information for resolving the issue.
|
||||
- Consider upstream and downstream dependencies that may affect or be affected by the issue.
|
||||
- If applicable, identify where to introduce new fields, functions, or variables.
|
||||
- Think Thoroughly: List multiple potential solutions and consider edge cases that could impact the resolution.
|
||||
|
||||
## Output Format for Final Results:
|
||||
Your final output should list the locations requiring modification, wrapped with triple backticks ```
|
||||
Each location should include the file path, class name (if applicable), function name, or line numbers, ordered by importance.
|
||||
Your answer would better include about 5 files.
|
||||
|
||||
### Examples:
|
||||
```
|
||||
full_path1/file1.py
|
||||
line: 10
|
||||
class: MyClass1
|
||||
function: my_function1
|
||||
|
||||
full_path2/file2.py
|
||||
line: 76
|
||||
function: MyClass2.my_function2
|
||||
|
||||
full_path3/file3.py
|
||||
line: 24
|
||||
line: 156
|
||||
function: my_function3
|
||||
```
|
||||
|
||||
Return just the location(s)
|
||||
|
||||
Note: Your thinking should be thorough and so it's fine if it's very long.
|
||||
"""
|
||||
|
||||
FAKE_USER_MSG_FOR_LOC = (
|
||||
'Verify if the found locations contain all the necessary information to address the issue, and check for any relevant references in other parts of the codebase that may not have appeared in the search results. '
|
||||
'If not, continue searching for additional locations related to the issue.\n'
|
||||
'Verify that you have carefully analyzed the impact of the found locations on the repository, especially their dependencies. '
|
||||
'If you think you have solved the task, please send your final answer (including the former answer and reranking) to user through message and then call `finish` to finish.\n'
|
||||
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
|
||||
)
|
||||
713
evaluation/benchmarks/swe_bench/run_localize.py
Normal file
713
evaluation/benchmarks/swe_bench/run_localize.py
Normal file
@ -0,0 +1,713 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
import toml
|
||||
from datasets import load_dataset
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.swe_bench.resource.mapping import (
|
||||
get_instance_resource_factor,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalException,
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
update_llm_config_for_completions_logging,
|
||||
)
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
AppConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import CmdRunAction, MessageAction
|
||||
from openhands.events.observation import CmdOutputObservation, ErrorObservation
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
|
||||
INDEX_BASE_DIR = os.environ.get('INDEX_BASE_DIR', '')
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': codeact_user_response,
|
||||
'LocAgent': codeact_user_response,
|
||||
}
|
||||
|
||||
|
||||
def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
|
||||
return f'{instance.repo}__{instance.version}'.replace('/', '__')
|
||||
|
||||
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
instruction = f"""
|
||||
Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{instance.problem_statement}
|
||||
</issue_description>
|
||||
|
||||
Your objective is to localize the specific files, classes or functions, and lines of code that need modification or contain key information to resolve the issue.
|
||||
|
||||
Follow these steps to localize the issue:
|
||||
## Step 1: Categorize and Extract Key Problem Information
|
||||
- Classify the problem statement into the following categories:
|
||||
Problem description, error trace, code to reproduce the bug, and additional context.
|
||||
- Identify modules in the "{instance.instance_id.split('_')[0]}" package mentioned in each category.
|
||||
- Use extracted keywords and line numbers to search for relevant code references for additional context.
|
||||
|
||||
## Step 2: Locate Referenced Modules
|
||||
- Accurately determine specific modules
|
||||
- Explore the repo to familiarize yourself with its structure.
|
||||
- Analyze the described execution flow to identify specific modules or components being referenced.
|
||||
- Pay special attention to distinguishing between modules with similar names using context and described execution flow.
|
||||
- Output Format for collected relevant modules:
|
||||
- Use the format: 'file_path:QualifiedName'
|
||||
- E.g., for a function `calculate_sum` in the `MathUtils` class located in `src/helpers/math_helpers.py`, represent it as: 'src/helpers/math_helpers.py:MathUtils.calculate_sum'.
|
||||
|
||||
## Step 3: Analyze and Reproducing the Problem
|
||||
- Clarify the Purpose of the Issue
|
||||
- If expanding capabilities: Identify where and how to incorporate new behavior, fields, or modules.
|
||||
- If addressing unexpected behavior: Focus on localizing modules containing potential bugs.
|
||||
- Reconstruct the execution flow
|
||||
- Identify main entry points triggering the issue.
|
||||
- Trace function calls, class interactions, and sequences of events.
|
||||
- Identify potential breakpoints causing the issue.
|
||||
Important: Keep the reconstructed flow focused on the problem, avoiding irrelevant details.
|
||||
|
||||
## Step 4: Locate Areas for Modification
|
||||
- Locate specific files, functions, or lines of code requiring changes or containing critical information for resolving the issue.
|
||||
- Consider upstream and downstream dependencies that may affect or be affected by the issue.
|
||||
- If applicable, identify where to introduce new fields, functions, or variables.
|
||||
- Think Thoroughly: List multiple potential solutions and consider edge cases that could impact the resolution.
|
||||
|
||||
## Output Format for Final Results:
|
||||
Your final output should list the locations requiring modification, wrapped with triple backticks ```
|
||||
Each location should include the file path, class name (if applicable), function name, or line numbers, ordered by importance.
|
||||
Your answer would better include about 5 files.
|
||||
|
||||
### Examples:
|
||||
```
|
||||
full_path1/file1.py
|
||||
line: 10
|
||||
class: MyClass1
|
||||
function: my_function1
|
||||
|
||||
full_path2/file2.py
|
||||
line: 76
|
||||
function: MyClass2.my_function2
|
||||
|
||||
full_path3/file3.py
|
||||
line: 24
|
||||
line: 156
|
||||
function: my_function3
|
||||
```
|
||||
|
||||
Return just the location(s)
|
||||
|
||||
Note: Your thinking should be thorough and so it's fine if it's very long.
|
||||
"""
|
||||
instruction += (
|
||||
'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
|
||||
"Don't include any lambda functions!\n"
|
||||
'You should NOT modify any files!\n'
|
||||
)
|
||||
if RUN_WITH_BROWSING:
|
||||
instruction += """
|
||||
<IMPORTANT!>
|
||||
You SHOULD NEVER attempt to browse the web.
|
||||
</IMPORTANT!>
|
||||
"""
|
||||
return instruction
|
||||
|
||||
|
||||
# TODO: migrate all swe-bench docker to ghcr.io/openhands
|
||||
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
|
||||
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
|
||||
)
|
||||
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
def get_instance_docker_image(instance_id: str, official_image: bool = False) -> str:
|
||||
if official_image:
|
||||
# Official SWE-Bench image
|
||||
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
|
||||
docker_image_prefix = 'docker.io/swebench/'
|
||||
repo, name = instance_id.split('__')
|
||||
image_name = f'sweb.eval.x86_64.{repo}_1776_{name}:latest'
|
||||
logger.warning(f'Using official SWE-Bench image: {image_name}')
|
||||
else:
|
||||
# OpenHands version of the image
|
||||
docker_image_prefix = DEFAULT_DOCKER_IMAGE_PREFIX
|
||||
image_name = 'sweb.eval.x86_64.' + instance_id
|
||||
image_name = image_name.replace(
|
||||
'__', '_s_'
|
||||
) # to comply with docker image naming convention
|
||||
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
|
||||
|
||||
|
||||
def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
# We use a different instance image for the each instance of swe-bench eval
|
||||
use_official_image = bool(
|
||||
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
|
||||
)
|
||||
base_container_image = get_instance_docker_image(
|
||||
instance['instance_id'], use_official_image
|
||||
)
|
||||
logger.info(
|
||||
f'Using instance container image: {base_container_image}. '
|
||||
f'Please make sure this image exists. '
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
sandbox_config.enable_auto_lint = True
|
||||
sandbox_config.use_host_network = False
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
sandbox_config.runtime_startup_env_vars = {
|
||||
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
|
||||
}
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
|
||||
def initialize_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required
|
||||
):
|
||||
"""Initialize the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
obs: CmdOutputObservation
|
||||
|
||||
# Set instance id
|
||||
action = CmdRunAction(
|
||||
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0, f'Failed to export SWE_INSTANCE_ID: {str(obs)}'
|
||||
)
|
||||
|
||||
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to export USER: {str(obs)}')
|
||||
|
||||
# inject the init script
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
# inject the instance info
|
||||
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to create /swe_util/eval_data/instances: {str(obs)}',
|
||||
)
|
||||
|
||||
swe_instance_json_name = 'swe-bench-instance.json'
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Construct the full path for the desired file name within the temporary directory
|
||||
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
|
||||
# Write to the file with the desired name within the temporary directory
|
||||
with open(temp_file_path, 'w') as f:
|
||||
if not isinstance(instance, dict):
|
||||
json.dump([instance.to_dict()], f)
|
||||
else:
|
||||
json.dump([instance], f)
|
||||
|
||||
# Copy the file to the desired location
|
||||
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
|
||||
|
||||
# inject the instance swe entry
|
||||
runtime.copy_to(
|
||||
str(os.path.join(script_dir, 'scripts/setup/instance_swe_entry.sh')),
|
||||
'/swe_util/',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='cat ~/.bashrc')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='source ~/.bashrc')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if isinstance(obs, ErrorObservation):
|
||||
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to source /swe_util/instance_swe_entry.sh: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='git reset --hard')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to git reset --hard: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(
|
||||
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
|
||||
|
||||
# Copy the processed indexes if available
|
||||
action = CmdRunAction(command='mkdir _index_data/graph_index_v2.3')
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
# Check if an existing graph index file is available
|
||||
graph_index_file_path = os.path.join(
|
||||
INDEX_BASE_DIR, 'graph_index_v2.3', f"{instance['instance_id']}.pkl"
|
||||
)
|
||||
if INDEX_BASE_DIR and os.path.exists(graph_index_file_path):
|
||||
logger.info(
|
||||
f"Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3"
|
||||
)
|
||||
|
||||
runtime.copy_to(
|
||||
graph_index_file_path,
|
||||
f'/workspace/{workspace_dir_name}/_index_data/graph_index_v2.3',
|
||||
)
|
||||
action = CmdRunAction(
|
||||
command=f'mv _index_data/graph_index_v2.3/{instance["instance_id"]}.pkl _index_data/graph_index_v2.3/code_graph.pkl'
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
bm25_index_dir = os.path.join(INDEX_BASE_DIR, 'BM25_index', instance['instance_id'])
|
||||
runtime.copy_to(
|
||||
bm25_index_dir, f'/workspace/{workspace_dir_name}/_index_data', recursive=True
|
||||
)
|
||||
action = CmdRunAction(
|
||||
command=f'mv _index_data/{instance["instance_id"]} _index_data/bm25_index'
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to mv file: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='which python')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0 and 'testbed' in obs.content,
|
||||
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
|
||||
)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
|
||||
|
||||
def complete_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
|
||||
) -> dict[str, Any]:
|
||||
"""Complete the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
If you need to do something in the sandbox to get the correctness metric after
|
||||
the agent has run, modify this function.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Completion Fn')
|
||||
logger.info('-' * 30)
|
||||
obs: CmdOutputObservation
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
if obs.exit_code == -1:
|
||||
# The previous command is still running
|
||||
# We need to kill previous command
|
||||
logger.info('The previous command is still running, trying to kill it...')
|
||||
action = CmdRunAction(command='C-c')
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
# Then run the command again
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='git config --global core.pager ""')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to git config --global core.pager "": {str(obs)}',
|
||||
)
|
||||
|
||||
# First check for any git repositories in subdirectories
|
||||
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to find git repositories: {str(obs)}',
|
||||
)
|
||||
|
||||
git_dirs = [p for p in obs.content.strip().split('\n') if p]
|
||||
if git_dirs:
|
||||
# Remove all .git directories in subdirectories
|
||||
for git_dir in git_dirs:
|
||||
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to remove git directory {git_dir}: {str(obs)}',
|
||||
)
|
||||
|
||||
# add all files
|
||||
action = CmdRunAction(command='git add -A')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to git add -A: {str(obs)}',
|
||||
)
|
||||
|
||||
n_retries = 0
|
||||
git_patch = None
|
||||
while n_retries < 5:
|
||||
action = CmdRunAction(
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]}'
|
||||
)
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
n_retries += 1
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
if obs.exit_code == 0:
|
||||
git_patch = obs.content.strip()
|
||||
break
|
||||
else:
|
||||
logger.info('Failed to get git diff, retrying...')
|
||||
sleep_if_should_continue(10)
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
logger.error(f'Error occurred: {obs.content}. Retrying...')
|
||||
sleep_if_should_continue(10)
|
||||
else:
|
||||
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
|
||||
|
||||
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Completion Fn')
|
||||
logger.info('-' * 30)
|
||||
return {'git_patch': git_patch}
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
runtime_failure_count: int = 0,
|
||||
) -> EvalOutput:
|
||||
config = get_config(instance, 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, instance.instance_id, log_dir)
|
||||
else:
|
||||
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
|
||||
|
||||
# Increase resource_factor with increasing attempt_id
|
||||
if runtime_failure_count > 0:
|
||||
config.sandbox.remote_runtime_resource_factor = min(
|
||||
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
|
||||
8,
|
||||
)
|
||||
logger.warning(
|
||||
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
|
||||
)
|
||||
runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
try:
|
||||
initialize_runtime(runtime, instance)
|
||||
|
||||
instruction = get_instruction(instance, metadata)
|
||||
|
||||
# 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=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
|
||||
metadata.agent_class
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# if fatal error, throw EvalError to trigger re-run
|
||||
if is_fatal_evaluation_error(state.last_error):
|
||||
raise EvalException('Fatal error detected: ' + state.last_error)
|
||||
|
||||
# ======= THIS IS SWE-Bench specific =======
|
||||
# Get git patch
|
||||
return_val = complete_runtime(runtime, instance)
|
||||
git_patch = return_val['git_patch']
|
||||
logger.info(
|
||||
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
|
||||
)
|
||||
finally:
|
||||
runtime.close()
|
||||
# ==========================================
|
||||
|
||||
# ======= Attempt to evaluate the agent's edits =======
|
||||
# we use eval_infer.sh to evaluate the agent's edits, not here
|
||||
# because the agent may alter the environment / testcases
|
||||
test_result = {
|
||||
'git_patch': git_patch,
|
||||
}
|
||||
|
||||
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
|
||||
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
|
||||
histories = [event_to_dict(event) for event in state.history]
|
||||
metrics = get_metrics(state)
|
||||
|
||||
# Save the output
|
||||
output = EvalOutput(
|
||||
instance_id=instance.instance_id,
|
||||
instruction=instruction,
|
||||
instance=instance.to_dict(), # SWE Bench specific
|
||||
test_result=test_result,
|
||||
metadata=metadata,
|
||||
history=histories,
|
||||
metrics=metrics,
|
||||
error=state.last_error if state and state.last_error else None,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
|
||||
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
data = toml.load(file)
|
||||
if 'selected_ids' in data:
|
||||
selected_ids = data['selected_ids']
|
||||
logger.info(
|
||||
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
|
||||
)
|
||||
subset = dataset[dataset[filter_column].isin(selected_ids)]
|
||||
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
|
||||
return subset
|
||||
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
|
||||
if len(skip_ids) > 0:
|
||||
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
|
||||
return dataset[~dataset[filter_column].isin(skip_ids)]
|
||||
return dataset
|
||||
|
||||
|
||||
# A list of instances that are known to be tricky to infer
|
||||
# (will cause runtime failure even with resource factor = 8)
|
||||
SWEGYM_EXCLUDE_IDS = [
|
||||
'dask__dask-10422',
|
||||
'pandas-dev__pandas-50548',
|
||||
'pandas-dev__pandas-53672',
|
||||
'pandas-dev__pandas-54174',
|
||||
'pandas-dev__pandas-55518',
|
||||
'pandas-dev__pandas-58383',
|
||||
'pydata__xarray-6721',
|
||||
'pytest-dev__pytest-10081',
|
||||
'pytest-dev__pytest-7236',
|
||||
]
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_parser()
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
default='princeton-nlp/SWE-bench',
|
||||
help='data set to evaluate on, either full-test or lite-test',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--split',
|
||||
type=str,
|
||||
default='test',
|
||||
help='split to evaluate on',
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
|
||||
# so we don't need to manage file uploading to OpenHands's repo
|
||||
dataset = load_dataset(args.dataset, split=args.split)
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
logger.info(
|
||||
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
|
||||
)
|
||||
if 'SWE-Gym' in args.dataset:
|
||||
swe_bench_tests = swe_bench_tests[
|
||||
~swe_bench_tests['instance_id'].isin(SWEGYM_EXCLUDE_IDS)
|
||||
]
|
||||
logger.info(
|
||||
f'{len(swe_bench_tests)} tasks left after excluding SWE-Gym excluded tasks'
|
||||
)
|
||||
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
llm_config.log_completions = True
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
details = {}
|
||||
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
|
||||
|
||||
dataset_descrption = (
|
||||
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
|
||||
)
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
dataset_descrption,
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
args.eval_output_dir,
|
||||
details=details,
|
||||
)
|
||||
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
print(f'### OUTPUT FILE: {output_file} ###')
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8 * 60 * 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
117
evaluation/benchmarks/swe_bench/scripts/run_localize.sh
Executable file
117
evaluation/benchmarks/swe_bench/scripts/run_localize.sh
Executable file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
MODEL_CONFIG=$1
|
||||
COMMIT_HASH=$2
|
||||
AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
MAX_ITER=$5
|
||||
NUM_WORKERS=$6
|
||||
DATASET=$7
|
||||
SPLIT=$8
|
||||
N_RUNS=$9
|
||||
|
||||
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
|
||||
|
||||
if [ -z "$MAX_ITER" ]; then
|
||||
echo "MAX_ITER not specified, use default 100"
|
||||
MAX_ITER=100
|
||||
fi
|
||||
|
||||
if [ -z "$RUN_WITH_BROWSING" ]; then
|
||||
echo "RUN_WITH_BROWSING not specified, use default false"
|
||||
RUN_WITH_BROWSING=false
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$DATASET" ]; then
|
||||
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
|
||||
DATASET="princeton-nlp/SWE-bench_Lite"
|
||||
fi
|
||||
|
||||
if [ -z "$SPLIT" ]; then
|
||||
echo "SPLIT not specified, use default test"
|
||||
SPLIT="test"
|
||||
fi
|
||||
|
||||
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
|
||||
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
|
||||
|
||||
get_openhands_version
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
echo "SPLIT: $SPLIT"
|
||||
|
||||
# Default to NOT use Hint
|
||||
if [ -z "$USE_HINT_TEXT" ]; then
|
||||
export USE_HINT_TEXT=false
|
||||
fi
|
||||
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
|
||||
EVAL_NOTE="$OPENHANDS_VERSION"
|
||||
# if not using Hint, add -no-hint to the eval note
|
||||
if [ "$USE_HINT_TEXT" = false ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-no-hint"
|
||||
fi
|
||||
|
||||
if [ "$RUN_WITH_BROWSING" = true ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-with-browsing"
|
||||
fi
|
||||
|
||||
if [ -n "$EXP_NAME" ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
|
||||
fi
|
||||
|
||||
function run_eval() {
|
||||
local eval_note=$1
|
||||
COMMAND="poetry run python evaluation/benchmarks/swe_bench/run_localize.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations $MAX_ITER \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $eval_note \
|
||||
--dataset $DATASET \
|
||||
--split $SPLIT"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
}
|
||||
|
||||
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
|
||||
if [ -z "$N_RUNS" ]; then
|
||||
N_RUNS=1
|
||||
echo "N_RUNS not specified, use default $N_RUNS"
|
||||
fi
|
||||
|
||||
# Skip runs if the run number is in the SKIP_RUNS list
|
||||
# read from env variable SKIP_RUNS as a comma separated list of run numbers
|
||||
SKIP_RUNS=(${SKIP_RUNS//,/ })
|
||||
for i in $(seq 1 $N_RUNS); do
|
||||
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
|
||||
echo "Skipping run $i"
|
||||
continue
|
||||
fi
|
||||
current_eval_note="$EVAL_NOTE-run_$i"
|
||||
echo "EVAL_NOTE: $current_eval_note"
|
||||
run_eval $current_eval_note
|
||||
done
|
||||
|
||||
checkout_original_branch
|
||||
@ -7,6 +7,7 @@ from openhands.agenthub import ( # noqa: E402
|
||||
browsing_agent,
|
||||
codeact_agent,
|
||||
dummy_agent,
|
||||
loc_agent,
|
||||
readonly_agent,
|
||||
visualbrowsing_agent,
|
||||
)
|
||||
@ -19,4 +20,5 @@ __all__ = [
|
||||
'browsing_agent',
|
||||
'visualbrowsing_agent',
|
||||
'readonly_agent',
|
||||
'loc_agent',
|
||||
]
|
||||
|
||||
@ -266,5 +266,5 @@ class CodeActAgent(Agent):
|
||||
|
||||
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
|
||||
return codeact_function_calling.response_to_actions(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys())
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys()),
|
||||
)
|
||||
|
||||
14
openhands/agenthub/loc_agent/README.md
Normal file
14
openhands/agenthub/loc_agent/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# LocAgent Framework
|
||||
|
||||
This folder is an implementation of Locagent. It is based on ([LocAgent](https://arxiv.org/abs/2503.09089), [tweet](https://x.com/XiangruTang/status/1900392655009333338)), a framework that addresses code localization through graph-based representation. By parsing codebases into directed heterogeneous graphs, LocAgent creates a lightweight representation that captures code structures and their dependencies, enabling LLM agents to effectively search and locate relevant entities through powerful multi-hop reasoning.
|
||||
|
||||
<!-- ## Overview -->
|
||||
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
The agent provides several built-in tools:
|
||||
|
||||
1. `search_code_snippets`
|
||||
2. `get_entity_contents`
|
||||
3. `explore_tree_structure`
|
||||
4
openhands/agenthub/loc_agent/__init__.py
Normal file
4
openhands/agenthub/loc_agent/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from openhands.agenthub.loc_agent.loc_agent import LocAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('LocAgent', LocAgent)
|
||||
126
openhands/agenthub/loc_agent/function_calling.py
Normal file
126
openhands/agenthub/loc_agent/function_calling.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""This file contains the function calling implementation for different actions.
|
||||
|
||||
This is similar to the functionality of `CodeActResponseParser`.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
from litellm import (
|
||||
ChatCompletionToolParam,
|
||||
ModelResponse,
|
||||
)
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools import FinishTool
|
||||
from openhands.agenthub.codeact_agent.function_calling import combine_thought
|
||||
from openhands.agenthub.loc_agent.tools import (
|
||||
SearchEntityTool,
|
||||
SearchRepoTool,
|
||||
create_explore_tree_structure_tool,
|
||||
)
|
||||
from openhands.core.exceptions import (
|
||||
FunctionCallNotExistsError,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
|
||||
|
||||
def response_to_actions(
|
||||
response: ModelResponse, mcp_tool_names: list[str] | None = None,
|
||||
) -> list[Action]:
|
||||
actions: list[Action] = []
|
||||
assert len(response.choices) == 1, 'Only one choice is supported for now'
|
||||
choice = response.choices[0]
|
||||
assistant_msg = choice.message
|
||||
|
||||
if hasattr(assistant_msg, 'tool_calls') and assistant_msg.tool_calls:
|
||||
# Check if there's assistant_msg.content. If so, add it to the thought
|
||||
thought = ''
|
||||
if isinstance(assistant_msg.content, str):
|
||||
thought = assistant_msg.content
|
||||
elif isinstance(assistant_msg.content, list):
|
||||
for msg in assistant_msg.content:
|
||||
if msg['type'] == 'text':
|
||||
thought += msg['text']
|
||||
|
||||
# Process each tool call to OpenHands action
|
||||
for i, tool_call in enumerate(assistant_msg.tool_calls):
|
||||
action: Action
|
||||
logger.debug(f'Tool call in function_calling.py: {tool_call}')
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise RuntimeError(
|
||||
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
|
||||
) from e
|
||||
|
||||
# ================================================
|
||||
# LocAgent's Tools
|
||||
# ================================================
|
||||
ALL_FUNCTIONS = [
|
||||
'explore_tree_structure',
|
||||
'search_code_snippets',
|
||||
'get_entity_contents',
|
||||
]
|
||||
if tool_call.function.name in ALL_FUNCTIONS:
|
||||
# We implement this in agent_skills, which can be used via Jupyter
|
||||
func_name = tool_call.function.name
|
||||
code = f'print({func_name}(**{arguments}))'
|
||||
logger.debug(f'TOOL CALL: {func_name} with code: {code}')
|
||||
action = IPythonRunCellAction(code=code)
|
||||
|
||||
# ================================================
|
||||
# AgentFinishAction
|
||||
# ================================================
|
||||
elif tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
)
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
|
||||
)
|
||||
|
||||
# We only add thought to the first action
|
||||
if i == 0:
|
||||
action = combine_thought(action, thought)
|
||||
# Add metadata for tool calling
|
||||
action.tool_call_metadata = ToolCallMetadata(
|
||||
tool_call_id=tool_call.id,
|
||||
function_name=tool_call.function.name,
|
||||
model_response=response,
|
||||
total_calls_in_response=len(assistant_msg.tool_calls),
|
||||
)
|
||||
actions.append(action)
|
||||
else:
|
||||
actions.append(
|
||||
MessageAction(
|
||||
content=str(assistant_msg.content) if assistant_msg.content else '',
|
||||
wait_for_response=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Add response id to actions
|
||||
# This will ensure we can match both actions without tool calls (e.g. MessageAction)
|
||||
# and actions with tool calls (e.g. CmdRunAction, IPythonRunCellAction, etc.)
|
||||
# with the token usage data
|
||||
for action in actions:
|
||||
action.response_id = response.id
|
||||
|
||||
assert len(actions) >= 1
|
||||
return actions
|
||||
|
||||
|
||||
def get_tools() -> list[ChatCompletionToolParam]:
|
||||
tools = [FinishTool]
|
||||
tools.append(SearchRepoTool)
|
||||
tools.append(SearchEntityTool)
|
||||
tools.append(create_explore_tree_structure_tool(use_simplified_description=True))
|
||||
return tools
|
||||
39
openhands/agenthub/loc_agent/loc_agent.py
Normal file
39
openhands/agenthub/loc_agent/loc_agent.py
Normal file
@ -0,0 +1,39 @@
|
||||
from openhands.agenthub.codeact_agent import CodeActAgent
|
||||
import openhands.agenthub.loc_agent.function_calling as locagent_function_calling
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm.llm import LLM
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from openhands.events.action import Action
|
||||
from openhands.llm.llm import ModelResponse
|
||||
|
||||
|
||||
class LocAgent(CodeActAgent):
|
||||
VERSION = '1.0'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the LocAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for the agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
|
||||
self.tools = locagent_function_calling.get_tools()
|
||||
logger.debug(
|
||||
f'TOOLS loaded for LocAgent: {", ".join([tool.get("function").get("name") for tool in self.tools])}'
|
||||
)
|
||||
|
||||
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
|
||||
return locagent_function_calling.response_to_actions(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys()),
|
||||
)
|
||||
8
openhands/agenthub/loc_agent/tools/__init__.py
Normal file
8
openhands/agenthub/loc_agent/tools/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .explore_structure import create_explore_tree_structure_tool
|
||||
from .search_content import SearchEntityTool, SearchRepoTool
|
||||
|
||||
__all__ = [
|
||||
'SearchEntityTool',
|
||||
'SearchRepoTool',
|
||||
'create_explore_tree_structure_tool',
|
||||
]
|
||||
185
openhands/agenthub/loc_agent/tools/explore_structure.py
Normal file
185
openhands/agenthub/loc_agent/tools/explore_structure.py
Normal file
@ -0,0 +1,185 @@
|
||||
from litellm import (
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionToolParamFunctionChunk,
|
||||
)
|
||||
|
||||
_SIMPLIFIED_STRUCTURE_EXPLORER_DESCRIPTION = """
|
||||
A unified tool that traverses a pre-built code graph to retrieve dependency structure around specified entities,
|
||||
with options to explore upstream or downstream, and control traversal depth and filters for entity and dependency types.
|
||||
"""
|
||||
|
||||
|
||||
_SIMPLIFIED_TREE_EXAMPLE = """
|
||||
Example Usage:
|
||||
1. Exploring Downstream Dependencies:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=['src/module_a.py:ClassA'],
|
||||
direction='downstream',
|
||||
traversal_depth=2,
|
||||
dependency_type_filter=['invokes', 'imports']
|
||||
)
|
||||
```
|
||||
2. Exploring the repository structure from the root directory (/) up to two levels deep:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=['/'],
|
||||
traversal_depth=2,
|
||||
dependency_type_filter=['contains']
|
||||
)
|
||||
```
|
||||
3. Generate Class Diagrams:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=selected_entity_ids,
|
||||
direction='both',
|
||||
traverse_depth=-1,
|
||||
dependency_type_filter=['inherits']
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
_DETAILED_STRUCTURE_EXPLORER_DESCRIPTION = """
|
||||
Unified repository exploring tool that traverses a pre-built code graph to retrieve dependency structure around specified entities.
|
||||
The search can be controlled to traverse upstream (exploring dependencies that entities rely on) or downstream (exploring how entities impact others), with optional limits on traversal depth and filters for entity and dependency types.
|
||||
|
||||
Code Graph Definition:
|
||||
* Entity Types: 'directory', 'file', 'class', 'function'.
|
||||
* Dependency Types: 'contains', 'imports', 'invokes', 'inherits'.
|
||||
* Hierarchy:
|
||||
- Directories contain files and subdirectories.
|
||||
- Files contain classes and functions.
|
||||
- Classes contain inner classes and methods.
|
||||
- Functions can contain inner functions.
|
||||
* Interactions:
|
||||
- Files/classes/functions can import classes and functions.
|
||||
- Classes can inherit from other classes.
|
||||
- Classes and functions can invoke others (invocations in a class's `__init__` are attributed to the class).
|
||||
Entity ID:
|
||||
* Unique identifier including file path and module path.
|
||||
* Here's an example of an Entity ID: `"interface/C.py:C.method_a.inner_func"` identifies function `inner_func` within `method_a` of class `C` in `"interface/C.py"`.
|
||||
|
||||
Notes:
|
||||
* Traversal Control: The `traversal_depth` parameter specifies how deep the function should explore the graph starting from the input entities.
|
||||
* Filtering: Use `entity_type_filter` and `dependency_type_filter` to narrow down the scope of the search, focusing on specific entity types and relationships.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
_DETAILED_TREE_EXAMPLE = """
|
||||
Example Usage:
|
||||
1. Exploring Outward Dependencies:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=['src/module_a.py:ClassA'],
|
||||
direction='downstream',
|
||||
traversal_depth=2,
|
||||
dependency_type_filter=['invokes', 'imports']
|
||||
)
|
||||
```
|
||||
This retrieves the dependencies of `ClassA` up to 2 levels deep, focusing only on classes and functions with 'invokes' and 'imports' relationships.
|
||||
|
||||
2. Exploring Inward Dependencies:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=['src/module_b.py:FunctionY'],
|
||||
direction='upstream',
|
||||
traversal_depth=-1
|
||||
)
|
||||
```
|
||||
This finds all entities that depend on `FunctionY` without restricting the traversal depth.
|
||||
3. Exploring Repository Structure:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=['/'],
|
||||
traversal_depth=2,
|
||||
dependency_type_filter=['contains']
|
||||
)
|
||||
```
|
||||
This retrieves the tree repository structure from the root directory (/), traversing up to two levels deep and focusing only on 'contains' relationship.
|
||||
4. Generate Class Diagrams:
|
||||
```
|
||||
explore_tree_structure(
|
||||
start_entities=selected_entity_ids,
|
||||
direction='both',
|
||||
traverse_depth=-1,
|
||||
dependency_type_filter=['inherits']
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
_STRUCTURE_EXPLORER_PARAMETERS = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'start_entities': {
|
||||
'description': (
|
||||
'List of entities (e.g., class, function, file, or directory paths) to begin the search from.\n'
|
||||
'Entities representing classes or functions must be formatted as "file_path:QualifiedName" (e.g., `interface/C.py:C.method_a.inner_func`).\n'
|
||||
'For files or directories, provide only the file or directory path (e.g., `src/module_a.py` or `src/`).'
|
||||
),
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
},
|
||||
'direction': {
|
||||
'description': (
|
||||
'Direction of traversal in the code graph; allowed options are: `upstream`, `downstream`, `both`.\n'
|
||||
"- 'upstream': Traversal to explore dependencies that the specified entities rely on (how they depend on others).\n"
|
||||
"- 'downstream': Traversal to explore the effects or interactions of the specified entities on others (how others depend on them).\n"
|
||||
"- 'both': Traversal on both direction."
|
||||
),
|
||||
'type': 'string',
|
||||
'enum': ['upstream', 'downstream', 'both'],
|
||||
'default': 'downstream',
|
||||
},
|
||||
'traversal_depth': {
|
||||
'description': (
|
||||
'Maximum depth of traversal. A value of -1 indicates unlimited depth (subject to a maximum limit).'
|
||||
'Must be either `-1` or a non-negative integer (≥ 0).'
|
||||
),
|
||||
'type': 'integer',
|
||||
'default': 2,
|
||||
},
|
||||
'entity_type_filter': {
|
||||
'description': (
|
||||
"List of entity types (e.g., 'class', 'function', 'file', 'directory') to include in the traversal. If None, all entity types are included."
|
||||
),
|
||||
'type': ['array', 'null'],
|
||||
'items': {'type': 'string'},
|
||||
'default': None,
|
||||
},
|
||||
'dependency_type_filter': {
|
||||
'description': (
|
||||
"List of dependency types (e.g., 'contains', 'imports', 'invokes', 'inherits') to include in the traversal. If None, all dependency types are included."
|
||||
),
|
||||
'type': ['array', 'null'],
|
||||
'items': {'type': 'string'},
|
||||
'default': None,
|
||||
},
|
||||
},
|
||||
'required': ['start_entities'],
|
||||
}
|
||||
|
||||
|
||||
def create_explore_tree_structure_tool(
|
||||
use_simplified_description: bool = False,
|
||||
) -> ChatCompletionToolParam:
|
||||
description = (
|
||||
_SIMPLIFIED_STRUCTURE_EXPLORER_DESCRIPTION
|
||||
if use_simplified_description
|
||||
else _DETAILED_STRUCTURE_EXPLORER_DESCRIPTION
|
||||
)
|
||||
example = (
|
||||
_SIMPLIFIED_TREE_EXAMPLE
|
||||
if use_simplified_description
|
||||
else _DETAILED_TREE_EXAMPLE
|
||||
)
|
||||
return ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='explore_tree_structure',
|
||||
description=description + example,
|
||||
parameters=_STRUCTURE_EXPLORER_PARAMETERS,
|
||||
),
|
||||
)
|
||||
98
openhands/agenthub/loc_agent/tools/search_content.py
Normal file
98
openhands/agenthub/loc_agent/tools/search_content.py
Normal file
@ -0,0 +1,98 @@
|
||||
from litellm import (
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionToolParamFunctionChunk,
|
||||
)
|
||||
|
||||
_SEARCH_ENTITY_DESCRIPTION = """
|
||||
Searches the codebase to retrieve the complete implementations of specified entities based on the provided entity names.
|
||||
The tool can handle specific entity queries such as function names, class names, or file paths.
|
||||
|
||||
**Usage Example:**
|
||||
# Search for a specific function implementation
|
||||
get_entity_contents(['src/my_file.py:MyClass.func_name'])
|
||||
|
||||
# Search for a file's complete content
|
||||
get_entity_contents(['src/my_file.py'])
|
||||
|
||||
**Entity Name Format:**
|
||||
- To specify a function or class, use the format: `file_path:QualifiedName`
|
||||
(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum').
|
||||
- To search for a file's content, use only the file path (e.g., 'src/my_file.py').
|
||||
"""
|
||||
|
||||
SearchEntityTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='get_entity_contents',
|
||||
description=_SEARCH_ENTITY_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'entity_names': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
'description': (
|
||||
'A list of entity names to query. Each entity name can represent a function, class, or file. '
|
||||
"For functions or classes, the format should be 'file_path:QualifiedName' "
|
||||
"(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum'). "
|
||||
"For files, use just the file path (e.g., 'src/my_file.py')."
|
||||
),
|
||||
}
|
||||
},
|
||||
'required': ['entity_names'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_SEARCH_REPO_DESCRIPTION = """Searches the codebase to retrieve relevant code snippets based on given queries(terms or line numbers).
|
||||
** Note:
|
||||
- Either `search_terms` or `line_nums` must be provided to perform a search.
|
||||
- If `search_terms` are provided, it searches for code snippets based on each term:
|
||||
- If `line_nums` is provided, it searches for code snippets around the specified lines within the file defined by `file_path_or_pattern`.
|
||||
|
||||
** Example Usage:
|
||||
# Search for code content contain keyword `order`, `bill`
|
||||
search_code_snippets(search_terms=["order", "bill"])
|
||||
|
||||
# Search for a class
|
||||
search_code_snippets(search_terms=["MyClass"])
|
||||
|
||||
# Search for context around specific lines (10 and 15) within a file
|
||||
search_code_snippets(line_nums=[10, 15], file_path_or_pattern='src/example.py')
|
||||
"""
|
||||
|
||||
SearchRepoTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='search_code_snippets',
|
||||
description=_SEARCH_REPO_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'search_terms': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
'description': 'A list of names, keywords, or code snippets to search for within the codebase. '
|
||||
'This can include potential function names, class names, or general code fragments. '
|
||||
'Either `search_terms` or `line_nums` must be provided to perform a search.',
|
||||
},
|
||||
'line_nums': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'integer'},
|
||||
'description': 'Specific line numbers to locate code snippets within a specified file. '
|
||||
'Must be used alongside a valid `file_path_or_pattern`. '
|
||||
'Either `line_nums` or `search_terms` must be provided to perform a search.',
|
||||
},
|
||||
'file_path_or_pattern': {
|
||||
'type': 'string',
|
||||
'description': 'A glob pattern or specific file path used to filter search results '
|
||||
'to particular files or directories. Defaults to "**/*.py", meaning all Python files are searched by default. '
|
||||
'If `line_nums` are provided, this must specify a specific file path.',
|
||||
'default': '**/*.py',
|
||||
},
|
||||
},
|
||||
'required': [],
|
||||
},
|
||||
),
|
||||
)
|
||||
@ -1,6 +1,6 @@
|
||||
from inspect import signature
|
||||
|
||||
from openhands.runtime.plugins.agent_skills import file_ops, file_reader
|
||||
from openhands.runtime.plugins.agent_skills import file_ops, file_reader, repo_ops
|
||||
from openhands.runtime.plugins.agent_skills.utils.dependency import import_functions
|
||||
|
||||
import_functions(
|
||||
@ -9,7 +9,11 @@ import_functions(
|
||||
import_functions(
|
||||
module=file_reader, function_names=file_reader.__all__, target_globals=globals()
|
||||
)
|
||||
__all__ = file_ops.__all__ + file_reader.__all__
|
||||
import_functions(
|
||||
module=repo_ops, function_names=repo_ops.__all__, target_globals=globals()
|
||||
)
|
||||
__all__ = file_ops.__all__ + file_reader.__all__ + repo_ops.__all__
|
||||
|
||||
|
||||
DOCUMENTATION = ''
|
||||
for func_name in __all__:
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
from openhands.runtime.plugins.agent_skills.repo_ops import repo_ops
|
||||
from openhands.runtime.plugins.agent_skills.utils.dependency import import_functions
|
||||
|
||||
import_functions(
|
||||
module=repo_ops, function_names=repo_ops.__all__, target_globals=globals()
|
||||
)
|
||||
__all__ = repo_ops.__all__
|
||||
11
openhands/runtime/plugins/agent_skills/repo_ops/repo_ops.py
Normal file
11
openhands/runtime/plugins/agent_skills/repo_ops/repo_ops.py
Normal file
@ -0,0 +1,11 @@
|
||||
from openhands_aci.indexing.locagent.tools import (
|
||||
explore_tree_structure,
|
||||
get_entity_contents,
|
||||
search_code_snippets,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'get_entity_contents',
|
||||
'search_code_snippets',
|
||||
'explore_tree_structure',
|
||||
]
|
||||
843
poetry.lock
generated
843
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,12 +20,12 @@ packages = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12,<3.14"
|
||||
litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272)
|
||||
aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13
|
||||
google-generativeai = "*" # To use litellm with Gemini Pro API
|
||||
google-api-python-client = "^2.164.0" # For Google Sheets API
|
||||
google-auth-httplib2 = "*" # For Google Sheets authentication
|
||||
google-auth-oauthlib = "*" # For Google Sheets OAuth
|
||||
litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272)
|
||||
aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13
|
||||
google-generativeai = "*" # To use litellm with Gemini Pro API
|
||||
google-api-python-client = "^2.164.0" # For Google Sheets API
|
||||
google-auth-httplib2 = "*" # For Google Sheets authentication
|
||||
google-auth-oauthlib = "*" # For Google Sheets OAuth
|
||||
termcolor = "*"
|
||||
docker = "*"
|
||||
fastapi = "*"
|
||||
@ -34,7 +34,7 @@ uvicorn = "*"
|
||||
types-toml = "*"
|
||||
numpy = "*"
|
||||
json-repair = "*"
|
||||
browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface
|
||||
browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface
|
||||
html2text = "*"
|
||||
e2b = ">=1.0.5,<1.4.0"
|
||||
pexpect = "*"
|
||||
@ -60,7 +60,7 @@ tornado = "*"
|
||||
python-dotenv = "*"
|
||||
pylcs = "^0.1.1"
|
||||
whatthepatch = "^1.0.6"
|
||||
protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+
|
||||
protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+
|
||||
opentelemetry-api = "1.25.0"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
|
||||
modal = ">=0.66.26,<0.78.0"
|
||||
@ -68,7 +68,7 @@ runloop-api-client = "0.32.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
pygithub = "^2.5.0"
|
||||
joblib = "*"
|
||||
openhands-aci = "0.2.13"
|
||||
openhands-aci = "0.2.14"
|
||||
python-socketio = "^5.11.4"
|
||||
redis = ">=5.2,<7.0"
|
||||
sse-starlette = "^2.1.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user