diff --git a/docs/modules/usage/mcp.md b/docs/modules/usage/mcp.md index de475b6462..4695542e24 100644 --- a/docs/modules/usage/mcp.md +++ b/docs/modules/usage/mcp.md @@ -20,7 +20,7 @@ MCP configuration is defined in the `[mcp]` section of your `config.toml` file. sse_servers = [ # Basic SSE server with just a URL "http://example.com:8080/mcp", - + # SSE server with API key authentication {url="https://secure-example.com/mcp", api_key="your-api-key"} ] @@ -29,7 +29,7 @@ sse_servers = [ stdio_servers = [ # Basic stdio server {name="fetch", command="uvx", args=["mcp-server-fetch"]}, - + # Stdio server with environment variables { name="data-processor", diff --git a/docs/package.json b/docs/package.json index 5bc8d2f03f..bd3a1bdec6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -55,4 +55,4 @@ "node": ">=18.0" }, "packageManager": "npm@10.5.0" -} \ No newline at end of file +} diff --git a/evaluation/benchmarks/multi_swe_bench/run_infer.py b/evaluation/benchmarks/multi_swe_bench/run_infer.py index 05f4bf594a..b09b6f7d34 100644 --- a/evaluation/benchmarks/multi_swe_bench/run_infer.py +++ b/evaluation/benchmarks/multi_swe_bench/run_infer.py @@ -36,13 +36,12 @@ from openhands.core.config import ( ) 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, FileReadAction +from openhands.events.action import CmdRunAction, FileReadAction, 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 -import pdb USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true' USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'true').lower() == 'true' @@ -51,7 +50,7 @@ RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'tru # TODO: migrate all swe-bench docker to ghcr.io/openhands # TODO: 适应所有的语言 DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', '') -LANGUAGE =os.environ.get('LANGUAGE', 'python') +LANGUAGE = os.environ.get('LANGUAGE', 'python') logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}') @@ -71,7 +70,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): # Instruction based on Anthropic's official trajectory # https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs instructions = { - "python":( + 'python': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -96,7 +95,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "java": ( + 'java': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -121,7 +120,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): " Make sure all these tests pass with your changes.\n" "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "go": ( + 'go': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -146,7 +145,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "c": ( + 'c': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -171,7 +170,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "cpp": ( + 'cpp': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -196,7 +195,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "javascript": ( + 'javascript': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -221,7 +220,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "typescript":( + 'typescript': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -246,7 +245,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" ), - "rust":( + 'rust': ( '\n' f'/workspace/{workspace_dir_name}\n' '\n' @@ -270,11 +269,10 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): ' - The functions you changed\n' ' Make sure all these tests pass with your changes.\n' "Your thinking should be thorough and so it's fine if it's very long.\n" - ) + ), } instruction = instructions.get(LANGUAGE.lower()) - if instruction and RUN_WITH_BROWSING: instruction += ( '\n' @@ -284,7 +282,6 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata): return instruction - # TODO: 适应所有的语言 # def get_instance_docker_image(instance_id: str) -> str: # image_name = 'sweb.eval.x86_64.' + instance_id @@ -307,16 +304,15 @@ def get_instance_docker_image(instance: pd.Series): container_name = container_name.replace('/', '_m_') instance_id = instance.get('instance_id', '') tag_suffix = instance_id.split('-')[-1] if instance_id else '' - container_tag = f"pr-{tag_suffix}" + container_tag = f'pr-{tag_suffix}' # pdb.set_trace() - return f"mswebench/{container_name}:{container_tag}" + return f'mswebench/{container_name}:{container_tag}' # return "kong/insomnia:pr-8284" # return "'sweb.eval.x86_64.local_insomnia" # return "local_insomnia_why" # return "local/kong-insomnia:pr-8117" - def get_config( instance: pd.Series, metadata: EvalMetadata, @@ -569,7 +565,6 @@ def complete_runtime( f'Failed to git config --global core.pager "": {str(obs)}', ) - action = CmdRunAction(command='git add -A') action.set_hard_timeout(600) logger.info(action, extra={'msg_type': 'ACTION'}) @@ -582,14 +577,14 @@ def complete_runtime( ##删除二进制文件 action = CmdRunAction( - command=f''' + command=""" for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then git rm -f "$file" 2>/dev/null || rm -f "$file" echo "Removed: $file" fi done - ''' + """ ) action.set_hard_timeout(600) logger.info(action, extra={'msg_type': 'ACTION'}) @@ -626,14 +621,12 @@ def complete_runtime( else: assert_and_raise(False, f'Unexpected observation type: {str(obs)}') - action = FileReadAction( - path='patch.diff' - ) + action = FileReadAction(path='patch.diff') action.set_hard_timeout(max(300 + 100 * n_retries, 600)) logger.info(action, extra={'msg_type': 'ACTION'}) obs = runtime.run_action(action) git_patch = obs.content - # pdb.set_trace() + # pdb.set_trace() assert_and_raise(git_patch is not None, 'Failed to get git diff (None)') @@ -714,12 +707,12 @@ def process_instance( is_binary_block = False for line in lines: - if line.startswith("diff --git "): + if line.startswith('diff --git '): if block and not is_binary_block: cleaned_lines.extend(block) block = [line] is_binary_block = False - elif "Binary files" in line: + elif 'Binary files' in line: is_binary_block = True block.append(line) else: @@ -727,7 +720,8 @@ def process_instance( if block and not is_binary_block: cleaned_lines.extend(block) - return "\n".join(cleaned_lines) + return '\n'.join(cleaned_lines) + git_patch = remove_binary_diffs(git_patch) test_result = { 'git_patch': git_patch, @@ -797,7 +791,7 @@ if __name__ == '__main__': # so we don't need to manage file uploading to OpenHands's repo # dataset = load_dataset(args.dataset, split=args.split) # dataset = load_dataset(args.dataset) - dataset = load_dataset("json", data_files = args.dataset) + dataset = load_dataset('json', data_files=args.dataset) dataset = dataset[args.split] swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id') logger.info( diff --git a/evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py b/evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py index 68833bbf02..298e9196ab 100644 --- a/evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py +++ b/evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py @@ -3,7 +3,9 @@ import json input_file = 'XXX.jsonl' output_file = 'YYY.jsonl' -with open(input_file, 'r', encoding='utf-8') as fin, open(output_file, 'w', encoding='utf-8') as fout: +with open(input_file, 'r', encoding='utf-8') as fin, open( + output_file, 'w', encoding='utf-8' +) as fout: for line in fin: line = line.strip() if not line: @@ -13,18 +15,22 @@ with open(input_file, 'r', encoding='utf-8') as fin, open(output_file, 'w', enco item = data # 提取原始数据 - org = item.get("org", "") - repo = item.get("repo", "") - number = str(item.get("number", "")) + org = item.get('org', '') + repo = item.get('repo', '') + number = str(item.get('number', '')) new_item = {} - new_item["repo"] = f"{org}/{repo}" - new_item["instance_id"] = f"{org}__{repo}-{number}" - new_item["problem_statement"] = item["resolved_issues"][0].get("title", "") + "\n" + item["resolved_issues"][0].get("body", "") - new_item["FAIL_TO_PASS"] = [] - new_item["PASS_TO_PASS"] = [] - new_item["base_commit"] = item['base'].get("sha","") - new_item["version"] = "0.1" # depends + new_item['repo'] = f'{org}/{repo}' + new_item['instance_id'] = f'{org}__{repo}-{number}' + new_item['problem_statement'] = ( + item['resolved_issues'][0].get('title', '') + + '\n' + + item['resolved_issues'][0].get('body', '') + ) + new_item['FAIL_TO_PASS'] = [] + new_item['PASS_TO_PASS'] = [] + new_item['base_commit'] = item['base'].get('sha', '') + new_item['version'] = '0.1' # depends output_data = new_item - fout.write(json.dumps(output_data, ensure_ascii=False) + "\n") + fout.write(json.dumps(output_data, ensure_ascii=False) + '\n') diff --git a/evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py b/evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py index a3fbe4e7e6..33c0631eff 100644 --- a/evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py +++ b/evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py @@ -15,7 +15,7 @@ def main(): 'org': groups.group(1), 'repo': groups.group(2), 'number': groups.group(3), - 'fix_patch': data['test_result']['git_patch'] + 'fix_patch': data['test_result']['git_patch'], } fout.write(json.dumps(patch) + '\n') diff --git a/frontend/__tests__/components/features/auth-modal.test.tsx b/frontend/__tests__/components/features/auth-modal.test.tsx index 2cc3c55a4d..1803e644ca 100644 --- a/frontend/__tests__/components/features/auth-modal.test.tsx +++ b/frontend/__tests__/components/features/auth-modal.test.tsx @@ -27,7 +27,7 @@ describe("AuthModal", () => { it("should render the GitHub and GitLab buttons", () => { render(); - + const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" }); const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" }); diff --git a/frontend/__tests__/routes/accept-tos.test.tsx b/frontend/__tests__/routes/accept-tos.test.tsx index d5ffe9f3d7..ce6f36793b 100644 --- a/frontend/__tests__/routes/accept-tos.test.tsx +++ b/frontend/__tests__/routes/accept-tos.test.tsx @@ -43,7 +43,7 @@ const createWrapper = () => { }, }, }); - + return ({ children }: { children: React.ReactNode }) => ( {children} ); @@ -61,7 +61,7 @@ describe("AcceptTOS", () => { it("should render a TOS checkbox that is unchecked by default", () => { render(, { wrapper: createWrapper() }); - + const checkbox = screen.getByRole("checkbox"); const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" }); @@ -72,7 +72,7 @@ describe("AcceptTOS", () => { it("should enable the continue button when the TOS checkbox is checked", async () => { const user = userEvent.setup(); render(, { wrapper: createWrapper() }); - + const checkbox = screen.getByRole("checkbox"); const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" }); @@ -96,7 +96,7 @@ describe("AcceptTOS", () => { const user = userEvent.setup(); render(, { wrapper: createWrapper() }); - + const checkbox = screen.getByRole("checkbox"); await user.click(checkbox); @@ -121,7 +121,7 @@ describe("AcceptTOS", () => { const user = userEvent.setup(); render(, { wrapper: createWrapper() }); - + const checkbox = screen.getByRole("checkbox"); await user.click(checkbox); @@ -133,4 +133,4 @@ describe("AcceptTOS", () => { expect(window.location.href).toBe(externalUrl); }); -}); \ No newline at end of file +}); diff --git a/openhands/integrations/github/github_service.py b/openhands/integrations/github/github_service.py index cc60094e27..a8babb1b2d 100644 --- a/openhands/integrations/github/github_service.py +++ b/openhands/integrations/github/github_service.py @@ -390,7 +390,9 @@ class GitHubService(BaseGitService, GitService): except Exception: return [] - async def get_repository_details_from_repo_name(self, repository: str) -> Repository: + async def get_repository_details_from_repo_name( + self, repository: str + ) -> Repository: url = f'{self.BASE_URL}/repos/{repository}' repo, _ = await self._make_request(url) diff --git a/openhands/integrations/gitlab/gitlab_service.py b/openhands/integrations/gitlab/gitlab_service.py index 5d28c970c6..ec30f93cb5 100644 --- a/openhands/integrations/gitlab/gitlab_service.py +++ b/openhands/integrations/gitlab/gitlab_service.py @@ -382,9 +382,10 @@ class GitLabService(BaseGitService, GitService): except Exception: return [] - - async def get_repository_details_from_repo_name(self, repository: str) -> Repository: - encoded_name = repository.replace("/", "%2F") + async def get_repository_details_from_repo_name( + self, repository: str + ) -> Repository: + encoded_name = repository.replace('/', '%2F') url = f'{self.BASE_URL}/projects/{encoded_name}' repo, _ = await self._make_request(url) @@ -396,8 +397,6 @@ class GitLabService(BaseGitService, GitService): git_provider=ProviderType.GITLAB, is_public=repo.get('visibility') == 'public', ) - - gitlab_service_cls = os.environ.get( diff --git a/openhands/integrations/templates/suggested_task/failing_checks_prompt.j2 b/openhands/integrations/templates/suggested_task/failing_checks_prompt.j2 index 8bab6638d6..8bcabea972 100644 --- a/openhands/integrations/templates/suggested_task/failing_checks_prompt.j2 +++ b/openhands/integrations/templates/suggested_task/failing_checks_prompt.j2 @@ -3,4 +3,4 @@ Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retriev Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention. Then use the {{ apiName }} to look at the {{ ciSystem }} that are failing on the most recent commit. Try and reproduce the failure locally. Get things working locally, then push your changes. Sleep for 30 seconds at a time until the {{ ciProvider }} {{ ciSystem.lower() }} have run again. -If they are still failing, repeat the process. \ No newline at end of file +If they are still failing, repeat the process. diff --git a/openhands/integrations/templates/suggested_task/merge_conflict_prompt.j2 b/openhands/integrations/templates/suggested_task/merge_conflict_prompt.j2 index 7fac70a8f6..945e49eda6 100644 --- a/openhands/integrations/templates/suggested_task/merge_conflict_prompt.j2 +++ b/openhands/integrations/templates/suggested_task/merge_conflict_prompt.j2 @@ -1,4 +1,4 @@ You are working on {{ requestType }} #{{ issue_number }} in repository {{ repo }}. You need to fix the merge conflicts. Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the {{ requestTypeShort }} details. Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention. -Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly. \ No newline at end of file +Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly. diff --git a/openhands/integrations/templates/suggested_task/open_issue_prompt.j2 b/openhands/integrations/templates/suggested_task/open_issue_prompt.j2 index b0d08d8d83..ea666f9966 100644 --- a/openhands/integrations/templates/suggested_task/open_issue_prompt.j2 +++ b/openhands/integrations/templates/suggested_task/open_issue_prompt.j2 @@ -1,4 +1,4 @@ You are working on Issue #{{ issue_number }} in repository {{ repo }}. Your goal is to fix the issue. Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the issue details and any comments on the issue. Then check out a new branch and investigate what changes will need to be made. -Finally, make the required changes and open up a {{ requestVerb }}. Be sure to reference the issue in the {{ requestTypeShort }} description. \ No newline at end of file +Finally, make the required changes and open up a {{ requestVerb }}. Be sure to reference the issue in the {{ requestTypeShort }} description. diff --git a/openhands/integrations/templates/suggested_task/unresolved_comments_prompt.j2 b/openhands/integrations/templates/suggested_task/unresolved_comments_prompt.j2 index 8b83b8af65..df9436cbff 100644 --- a/openhands/integrations/templates/suggested_task/unresolved_comments_prompt.j2 +++ b/openhands/integrations/templates/suggested_task/unresolved_comments_prompt.j2 @@ -2,4 +2,4 @@ You are working on {{ requestType }} #{{ issue_number }} in repository {{ repo } Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the {{ requestTypeShort }} details. Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention. Then use the {{ apiName }} to retrieve all the feedback on the {{ requestTypeShort }} so far. -If anything hasn't been addressed, address it and commit your changes back to the same branch. \ No newline at end of file +If anything hasn't been addressed, address it and commit your changes back to the same branch. diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index 91469de9ee..95dc8eac77 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -1,4 +1,3 @@ -import asyncio import os import tempfile import threading @@ -46,6 +45,7 @@ from openhands.runtime.utils.request import send_request from openhands.utils.http_session import HttpSession from openhands.utils.tenacity_stop import stop_if_should_exit + def _is_retryable_error(exception): return isinstance( exception, (httpx.RemoteProtocolError, httpcore.RemoteProtocolError) @@ -358,26 +358,27 @@ class ActionExecutionClient(Runtime): async def call_tool_mcp(self, action: MCPAction) -> Observation: # Import here to avoid circular imports - from openhands.mcp.utils import create_mcp_clients, call_tool_mcp as call_tool_mcp_handler - + from openhands.mcp.utils import call_tool_mcp as call_tool_mcp_handler + from openhands.mcp.utils import create_mcp_clients + # Get the updated MCP config updated_mcp_config = self.get_updated_mcp_config() self.log( 'debug', f'Creating MCP clients with servers: {updated_mcp_config.sse_servers}', ) - + # Create clients for this specific operation mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers) - + # Call the tool and return the result # No need for try/finally since disconnect() is now just resetting state result = await call_tool_mcp_handler(mcp_clients, action) - + # Reset client state (no active connections to worry about) for client in mcp_clients: await client.disconnect() - + return result def close(self) -> None: diff --git a/openhands/server/conversation_manager/conversation_manager.py b/openhands/server/conversation_manager/conversation_manager.py index 355263d522..6f7e5716e8 100644 --- a/openhands/server/conversation_manager/conversation_manager.py +++ b/openhands/server/conversation_manager/conversation_manager.py @@ -10,8 +10,8 @@ from openhands.events.event_store import EventStore from openhands.server.config.server_config import ServerConfig from openhands.server.monitoring import MonitoringListener from openhands.server.session.conversation import Conversation -from openhands.storage.data_models.settings import Settings from openhands.storage.conversation.conversation_store import ConversationStore +from openhands.storage.data_models.settings import Settings from openhands.storage.files import FileStore diff --git a/openhands/server/conversation_manager/standalone_conversation_manager.py b/openhands/server/conversation_manager/standalone_conversation_manager.py index 2c2387a62d..9cc2df5ffe 100644 --- a/openhands/server/conversation_manager/standalone_conversation_manager.py +++ b/openhands/server/conversation_manager/standalone_conversation_manager.py @@ -18,9 +18,9 @@ from openhands.server.monitoring import MonitoringListener from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE from openhands.server.session.conversation import Conversation from openhands.server.session.session import ROOM_KEY, Session -from openhands.storage.data_models.settings import Settings from openhands.storage.conversation.conversation_store import ConversationStore from openhands.storage.data_models.conversation_metadata import ConversationMetadata +from openhands.storage.data_models.settings import Settings from openhands.storage.files import FileStore from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync, wait_all from openhands.utils.import_utils import get_impl diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index baa5fb6dbd..3f0b8ca255 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -14,7 +14,11 @@ from openhands.integrations.provider import ( PROVIDER_TOKEN_TYPE, ProviderHandler, ) -from openhands.integrations.service_types import AuthenticationError, ProviderType, Repository, SuggestedTask +from openhands.integrations.service_types import ( + AuthenticationError, + ProviderType, + SuggestedTask, +) from openhands.runtime import get_runtime_cls from openhands.server.data_models.conversation_info import ConversationInfo from openhands.server.data_models.conversation_info_result_set import ( @@ -45,7 +49,6 @@ from openhands.storage.data_models.conversation_status import ConversationStatus from openhands.utils.async_utils import wait_all from openhands.utils.conversation_summary import generate_conversation_title - app = APIRouter(prefix='/api') @@ -58,7 +61,7 @@ class InitSessionRequest(BaseModel): image_urls: list[str] | None = None replay_json: str | None = None suggested_task: SuggestedTask | None = None - + async def _create_new_conversation( user_id: str | None, @@ -71,10 +74,13 @@ async def _create_new_conversation( conversation_trigger: ConversationTrigger = ConversationTrigger.GUI, attach_convo_id: bool = False, ): - logger.info( 'Creating conversation', - extra={'signal': 'create_conversation', 'user_id': user_id, 'trigger': conversation_trigger.value}, + extra={ + 'signal': 'create_conversation', + 'user_id': user_id, + 'trigger': conversation_trigger.value, + }, ) logger.info('Loading settings') settings_store = await SettingsStoreImpl.get_instance(config, user_id) @@ -163,7 +169,7 @@ async def new_conversation( data: InitSessionRequest, user_id: str = Depends(get_user_id), provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens), - auth_type: AuthType | None = Depends(get_auth_type) + auth_type: AuthType | None = Depends(get_auth_type), ): """Initialize a new session or join an existing one. @@ -202,7 +208,7 @@ async def new_conversation( initial_user_msg=initial_user_msg, image_urls=image_urls, replay_json=replay_json, - conversation_trigger=conversation_trigger + conversation_trigger=conversation_trigger, ) return JSONResponse( @@ -227,13 +233,13 @@ async def new_conversation( }, status_code=status.HTTP_400_BAD_REQUEST, ) - + except AuthenticationError as e: return JSONResponse( content={ 'status': 'error', 'message': str(e), - 'msg_id': 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR' + 'msg_id': 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR', }, status_code=status.HTTP_400_BAD_REQUEST, ) diff --git a/openhands/server/routes/public.py b/openhands/server/routes/public.py index 425681abed..d210582a4c 100644 --- a/openhands/server/routes/public.py +++ b/openhands/server/routes/public.py @@ -2,9 +2,8 @@ from typing import Any from fastapi import APIRouter -from openhands.security.options import SecurityAnalyzers - from openhands.controller.agent import Agent +from openhands.security.options import SecurityAnalyzers from openhands.server.shared import config, server_config from openhands.utils.llm import get_supported_llm_models diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 7b80c60dd7..4428739d71 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -15,12 +15,12 @@ from openhands.server.settings import ( POSTSettingsModel, ) from openhands.server.shared import config -from openhands.storage.data_models.settings import Settings from openhands.server.user_auth import ( get_provider_tokens, get_user_settings, get_user_settings_store, ) +from openhands.storage.data_models.settings import Settings from openhands.storage.settings.settings_store import SettingsStore app = APIRouter(prefix='/api') @@ -38,7 +38,7 @@ async def load_settings( content={'error': 'Settings not found'}, ) - provider_tokens_set: dict[ProviderType, str | None] = {} + provider_tokens_set: dict[ProviderType, str | None] = {} if provider_tokens: for provider_type, provider_token in provider_tokens.items(): if provider_token.token or provider_token.user_id: @@ -227,8 +227,7 @@ async def store_provider_tokens( if existing_settings: if existing_settings.secrets_store: existing_providers = [ - provider - for provider in existing_settings.secrets_store.provider_tokens + provider for provider in existing_settings.secrets_store.provider_tokens ] # Merge incoming settings store with the existing one @@ -245,7 +244,7 @@ async def store_provider_tokens( else: # nothing passed in means keep current settings provider_tokens = dict(existing_settings.secrets_store.provider_tokens) settings.provider_tokens = provider_tokens - + return settings @@ -334,7 +333,11 @@ def convert_to_settings(settings_with_token_data: POSTSettingsModel) -> Settings # Create new provider tokens immutably if settings_with_token_data.provider_tokens: settings = settings.model_copy( - update={'secrets_store': SecretStore(provider_tokens=settings_with_token_data.provider_tokens)} + update={ + 'secrets_store': SecretStore( + provider_tokens=settings_with_token_data.provider_tokens + ) + } ) return settings diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index 0cdd7d7d5e..a46f45b67b 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -17,7 +17,6 @@ from openhands.events.action import ChangeAgentStateAction, MessageAction from openhands.events.event import Event, EventSource from openhands.events.stream import EventStream from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler -from openhands.integrations.service_types import Repository from openhands.mcp import add_mcp_tools_to_agent from openhands.memory.memory import Memory from openhands.microagent.microagent import BaseMicroagent @@ -420,9 +419,7 @@ class AgentSession: memory.load_user_workspace_microagents(microagents) if selected_repository and repo_directory: - memory.set_repository_info( - selected_repository, repo_directory - ) + memory.set_repository_info(selected_repository, repo_directory) return memory def _maybe_restore_state(self) -> State | None: diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index 951bbe0e49..53b9ca5473 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -21,8 +21,8 @@ from openhands.events.observation import ( CmdOutputObservation, NullObservation, ) -from openhands.events.observation.error import ErrorObservation from openhands.events.observation.agent import RecallObservation +from openhands.events.observation.error import ErrorObservation from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.stream import EventStreamSubscriber from openhands.llm.llm import LLM @@ -214,7 +214,8 @@ class Session: await self.send(event_to_dict(event)) # NOTE: ipython observations are not sent here currently elif event.source == EventSource.ENVIRONMENT and isinstance( - event, (CmdOutputObservation, AgentStateChangedObservation, RecallObservation) + event, + (CmdOutputObservation, AgentStateChangedObservation, RecallObservation), ): # feedback from the environment to agent actions is understood as agent events by the UI event_dict = event_to_dict(event) diff --git a/openhands/server/user_auth/__init__.py b/openhands/server/user_auth/__init__.py index 091f8ad26b..e574552b66 100644 --- a/openhands/server/user_auth/__init__.py +++ b/openhands/server/user_auth/__init__.py @@ -50,4 +50,4 @@ async def get_user_settings_store(request: Request) -> SettingsStore | None: async def get_auth_type(request: Request) -> AuthType | None: user_auth = await get_user_auth(request) - return user_auth.get_auth_type() \ No newline at end of file + return user_auth.get_auth_type() diff --git a/openhands/server/user_auth/default_user_auth.py b/openhands/server/user_auth/default_user_auth.py index 9ac2acebf9..e46880cb34 100644 --- a/openhands/server/user_auth/default_user_auth.py +++ b/openhands/server/user_auth/default_user_auth.py @@ -51,7 +51,6 @@ class DefaultUserAuth(UserAuth): provider_tokens = getattr(secrets_store, 'provider_tokens', None) return provider_tokens - @classmethod async def get_instance(cls, request: Request) -> UserAuth: user_auth = DefaultUserAuth() diff --git a/openhands/server/user_auth/user_auth.py b/openhands/server/user_auth/user_auth.py index 2902cfc4ae..f955a73b15 100644 --- a/openhands/server/user_auth/user_auth.py +++ b/openhands/server/user_auth/user_auth.py @@ -14,8 +14,8 @@ from openhands.utils.import_utils import get_impl class AuthType(Enum): - COOKIE = "cookie" - BEARER = "bearer" + COOKIE = 'cookie' + BEARER = 'bearer' class UserAuth(ABC): diff --git a/openhands/storage/settings/file_settings_store.py b/openhands/storage/settings/file_settings_store.py index ba832e3acb..88447d41bb 100644 --- a/openhands/storage/settings/file_settings_store.py +++ b/openhands/storage/settings/file_settings_store.py @@ -4,8 +4,8 @@ import json from dataclasses import dataclass from openhands.core.config.app_config import AppConfig -from openhands.storage.data_models.settings import Settings from openhands.storage import get_file_store +from openhands.storage.data_models.settings import Settings from openhands.storage.files import FileStore from openhands.storage.settings.settings_store import SettingsStore from openhands.utils.async_utils import call_sync_from_async diff --git a/tests/unit/resolver/github/test_resolve_issues.py b/tests/unit/resolver/github/test_resolve_issues.py index 00ebd13e6f..99f84a6994 100644 --- a/tests/unit/resolver/github/test_resolve_issues.py +++ b/tests/unit/resolver/github/test_resolve_issues.py @@ -26,7 +26,7 @@ from openhands.resolver.resolver_output import ResolverOutput @pytest.fixture def default_mock_args(): """Fixture that provides a default mock args object with common values. - + Tests can override specific attributes as needed. """ mock_args = MagicMock() @@ -53,10 +53,13 @@ def default_mock_args(): @pytest.fixture def mock_github_token(): """Fixture that patches the identify_token function to return GitHub provider type. - + This eliminates the need for repeated patching in each test function. """ - with patch('openhands.resolver.resolve_issue.identify_token', return_value=ProviderType.GITHUB) as patched: + with patch( + 'openhands.resolver.resolve_issue.identify_token', + return_value=ProviderType.GITHUB, + ) as patched: yield patched @@ -152,7 +155,9 @@ async def test_resolve_issue_no_issues_found(default_mock_args, mock_github_toke # Verify that the handler was correctly configured and called resolver.issue_handler_factory.assert_called_once() - mock_handler.get_converted_issues.assert_called_once_with(issue_numbers=[5432], comment_id=None) + mock_handler.get_converted_issues.assert_called_once_with( + issue_numbers=[5432], comment_id=None + ) def test_download_issues_from_github(): @@ -348,9 +353,7 @@ async def test_complete_runtime(default_mock_args, mock_github_token): # Create resolver with mocked token identification resolver = IssueResolver(default_mock_args) - result = await resolver.complete_runtime( - mock_runtime, 'base_commit_hash' - ) + result = await resolver.complete_runtime(mock_runtime, 'base_commit_hash') assert result == {'git_patch': 'git diff content'} assert mock_runtime.run_action.call_count == 5 @@ -358,7 +361,7 @@ async def test_complete_runtime(default_mock_args, mock_github_token): @pytest.mark.asyncio @pytest.mark.parametrize( - "test_case", + 'test_case', [ { 'name': 'successful_run', @@ -410,11 +413,20 @@ async def test_complete_runtime(default_mock_args, mock_github_token): 'expected_error': None, 'expected_explanation': 'Non-JSON explanation', 'is_pr': True, - 'comment_success': [True, False], # To trigger the PR success logging code path + 'comment_success': [ + True, + False, + ], # To trigger the PR success logging code path }, ], ) -async def test_process_issue(default_mock_args, mock_github_token, mock_output_dir, mock_prompt_template, test_case): +async def test_process_issue( + default_mock_args, + mock_github_token, + mock_output_dir, + mock_prompt_template, + test_case, +): """Test the process_issue method with different scenarios.""" # Set up test data @@ -426,7 +438,7 @@ async def test_process_issue(default_mock_args, mock_github_token, mock_output_d body='This is a test issue', ) base_commit = 'abcdef1234567890' - + # Customize the mock args for this test default_mock_args.output_dir = mock_output_dir default_mock_args.issue_type = 'pr' if test_case.get('is_pr', False) else 'issue' @@ -457,7 +469,7 @@ async def test_process_issue(default_mock_args, mock_github_token, mock_output_d # Mock the create_runtime function mock_create_runtime = MagicMock(return_value=mock_runtime) - + # Mock the run_controller function mock_run_controller = AsyncMock() if test_case['run_controller_raises']: @@ -466,14 +478,15 @@ async def test_process_issue(default_mock_args, mock_github_token, mock_output_d mock_run_controller.return_value = test_case['run_controller_return'] # Patch the necessary functions and methods - with patch('openhands.resolver.resolve_issue.create_runtime', mock_create_runtime), \ - patch('openhands.resolver.resolve_issue.run_controller', mock_run_controller), \ - patch.object(resolver, 'complete_runtime', return_value={'git_patch': 'test patch'}), \ - patch.object(resolver, 'initialize_runtime') as mock_initialize_runtime: - + with patch( + 'openhands.resolver.resolve_issue.create_runtime', mock_create_runtime + ), patch( + 'openhands.resolver.resolve_issue.run_controller', mock_run_controller + ), patch.object( + resolver, 'complete_runtime', return_value={'git_patch': 'test patch'} + ), patch.object(resolver, 'initialize_runtime') as mock_initialize_runtime: # Call the process_issue method result = await resolver.process_issue(issue, base_commit, handler_instance) - # Assert the result matches our expectations assert isinstance(result, ResolverOutput) @@ -490,16 +503,17 @@ async def test_process_issue(default_mock_args, mock_github_token, mock_output_d mock_initialize_runtime.assert_called_once() mock_run_controller.assert_called_once() resolver.complete_runtime.assert_awaited_once_with(mock_runtime, base_commit) - + # Assert run_controller was called with the right parameters if not test_case['run_controller_raises']: # Check that the first positional argument is a config assert 'config' in mock_run_controller.call_args[1] # Check that initial_user_action is a MessageAction with the right content - assert isinstance(mock_run_controller.call_args[1]['initial_user_action'], MessageAction) + assert isinstance( + mock_run_controller.call_args[1]['initial_user_action'], MessageAction + ) assert mock_run_controller.call_args[1]['runtime'] == mock_runtime - - + # Assert that guess_success was called only for successful runs if test_case['expected_success']: handler_instance.guess_success.assert_called_once() diff --git a/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py b/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py index 17661c09f1..f7145904b4 100644 --- a/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py +++ b/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py @@ -19,14 +19,16 @@ from openhands.resolver.interfaces.issue_definitions import ( ServiceContextIssue, ServiceContextPR, ) -from openhands.resolver.resolve_issue import IssueResolver, SandboxConfig, AppConfig, AgentConfig +from openhands.resolver.resolve_issue import ( + IssueResolver, +) from openhands.resolver.resolver_output import ResolverOutput @pytest.fixture def default_mock_args(): """Fixture that provides a default mock args object with common values. - + Tests can override specific attributes as needed. """ mock_args = MagicMock() @@ -52,10 +54,13 @@ def default_mock_args(): @pytest.fixture def mock_gitlab_token(): """Fixture that patches the identify_token function to return GitLab provider type. - + This eliminates the need for repeated patching in each test function. """ - with patch('openhands.resolver.resolve_issue.identify_token', return_value=ProviderType.GITLAB) as patched: + with patch( + 'openhands.resolver.resolve_issue.identify_token', + return_value=ProviderType.GITLAB, + ) as patched: yield patched @@ -124,10 +129,10 @@ def test_initialize_runtime(default_mock_args, mock_gitlab_token): exit_code=0, content='', command='git config --global core.pager ""' ), ] - + # Create resolver with mocked token identification resolver = IssueResolver(default_mock_args) - + resolver.initialize_runtime(mock_runtime) if os.getenv('GITLAB_CI') == 'true': @@ -154,24 +159,26 @@ async def test_resolve_issue_no_issues_found(default_mock_args, mock_gitlab_toke # Customize the mock args for this test default_mock_args.issue_number = 5432 - + # Create a resolver instance with mocked token identification resolver = IssueResolver(default_mock_args) - + # Mock the issue_handler_factory method resolver.issue_handler_factory = MagicMock(return_value=mock_handler) - + # Test that the correct exception is raised with pytest.raises(ValueError) as exc_info: await resolver.resolve_issue() - + # Verify the error message assert 'No issues found for issue number 5432' in str(exc_info.value) assert 'test-owner/test-repo' in str(exc_info.value) - + # Verify that the handler was correctly configured and called resolver.issue_handler_factory.assert_called_once() - mock_handler.get_converted_issues.assert_called_once_with(issue_numbers=[5432], comment_id=None) + mock_handler.get_converted_issues.assert_called_once_with( + issue_numbers=[5432], comment_id=None + ) def test_download_issues_from_gitlab(): @@ -377,12 +384,14 @@ async def test_complete_runtime(default_mock_args, mock_gitlab_token): content='', command='git config --global --add safe.directory /workspace', ), + create_cmd_output(exit_code=0, content='', command='git add -A'), create_cmd_output( - exit_code=0, content='', command='git add -A' + exit_code=0, + content='git diff content', + command='git diff --no-color --cached base_commit_hash', ), - create_cmd_output(exit_code=0, content='git diff content', command='git diff --no-color --cached base_commit_hash'), ] - + # Create a resolver instance with mocked token identification resolver = IssueResolver(default_mock_args) @@ -394,7 +403,7 @@ async def test_complete_runtime(default_mock_args, mock_gitlab_token): @pytest.mark.asyncio @pytest.mark.parametrize( - "test_case", + 'test_case', [ { 'name': 'successful_run', @@ -448,7 +457,13 @@ async def test_complete_runtime(default_mock_args, mock_gitlab_token): }, ], ) -async def test_process_issue(default_mock_args, mock_gitlab_token, mock_output_dir, mock_prompt_template, test_case): +async def test_process_issue( + default_mock_args, + mock_gitlab_token, + mock_output_dir, + mock_prompt_template, + test_case, +): """Test the process_issue method with different scenarios.""" # Set up test data issue = Issue( @@ -482,7 +497,7 @@ async def test_process_issue(default_mock_args, mock_gitlab_token, mock_output_d mock_runtime = MagicMock() mock_runtime.connect = AsyncMock() mock_create_runtime = MagicMock(return_value=mock_runtime) - + # Configure run_controller mock based on test case mock_run_controller = AsyncMock() if test_case.get('run_controller_raises'): @@ -491,16 +506,18 @@ async def test_process_issue(default_mock_args, mock_gitlab_token, mock_output_d mock_run_controller.return_value = test_case['run_controller_return'] # Patch the necessary functions and methods - with patch('openhands.resolver.resolve_issue.create_runtime', mock_create_runtime), \ - patch('openhands.resolver.resolve_issue.run_controller', mock_run_controller), \ - patch.object(resolver, 'complete_runtime', return_value={'git_patch': 'test patch'}), \ - patch.object(resolver, 'initialize_runtime') as mock_initialize_runtime, \ - patch('openhands.resolver.resolve_issue.SandboxConfig', return_value=MagicMock()), \ - patch('openhands.resolver.resolve_issue.AppConfig', return_value=MagicMock()): - + with patch( + 'openhands.resolver.resolve_issue.create_runtime', mock_create_runtime + ), patch( + 'openhands.resolver.resolve_issue.run_controller', mock_run_controller + ), patch.object( + resolver, 'complete_runtime', return_value={'git_patch': 'test patch'} + ), patch.object(resolver, 'initialize_runtime') as mock_initialize_runtime, patch( + 'openhands.resolver.resolve_issue.SandboxConfig', return_value=MagicMock() + ), patch('openhands.resolver.resolve_issue.AppConfig', return_value=MagicMock()): # Call the process_issue method result = await resolver.process_issue(issue, base_commit, handler_instance) - + mock_create_runtime.assert_called_once() mock_runtime.connect.assert_called_once() mock_initialize_runtime.assert_called_once() @@ -521,6 +538,7 @@ async def test_process_issue(default_mock_args, mock_gitlab_token, mock_output_d else: handler_instance.guess_success.assert_not_called() + def test_get_instruction(mock_prompt_template, mock_followup_prompt_template): issue = Issue( owner='test_owner', @@ -923,4 +941,4 @@ def test_download_issue_with_specific_comment(): if __name__ == '__main__': - pytest.main() \ No newline at end of file + pytest.main()