mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
feat/azure
...
update-wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a1c27e9e |
@@ -12,8 +12,5 @@
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
},
|
||||
"postCreateCommand": ".devcontainer/setup.sh",
|
||||
"runArgs": ["--add-host=host.docker.internal:host-gateway"],
|
||||
"containerEnv": {
|
||||
"DOCKER_HOST_ADDR": "host.docker.internal"
|
||||
},
|
||||
"runArgs": ["--network=host"],
|
||||
}
|
||||
|
||||
2
.github/workflows/lint-fix.yml
vendored
2
.github/workflows/lint-fix.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Fix python lint issues
|
||||
run: |
|
||||
# Run all pre-commit hooks and continue even if they modify files (exit code 1)
|
||||
pre-commit run --config ./dev_config/python/.pre-commit-config.yaml --all-files || true
|
||||
pre-commit run --config ./dev_config/python/.pre-commit-config.yaml --files openhands/**/* evaluation/**/* tests/**/* || true
|
||||
|
||||
# Commit and push changes if any
|
||||
- name: Check for changes
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
run: pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
|
||||
1
.github/workflows/py-unit-tests.yml
vendored
1
.github/workflows/py-unit-tests.yml
vendored
@@ -81,3 +81,4 @@ jobs:
|
||||
env:
|
||||
TEST_RUNTIME: local
|
||||
DEBUG: "1"
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.43-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.42-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -189,7 +189,7 @@ install-pre-commit-hooks:
|
||||
|
||||
lint-backend:
|
||||
@echo "$(YELLOW)Running linters...$(RESET)"
|
||||
@poetry run pre-commit run --all-files --show-diff-on-failure --config $(PRE_COMMIT_CONFIG_PATH)
|
||||
@poetry run pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config $(PRE_COMMIT_CONFIG_PATH)
|
||||
|
||||
lint-frontend:
|
||||
@echo "$(YELLOW)Running linters for frontend...$(RESET)"
|
||||
|
||||
22
README.md
22
README.md
@@ -20,15 +20,15 @@
|
||||
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
|
||||
|
||||
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
|
||||
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=zh">中文</a>
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
@@ -62,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
```
|
||||
|
||||
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands!
|
||||
|
||||
@@ -10,9 +10,8 @@ services:
|
||||
environment:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.43-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.42-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
npm install -g mint
|
||||
```
|
||||
|
||||
or
|
||||
or
|
||||
|
||||
```
|
||||
yarn global add mint
|
||||
@@ -14,4 +14,4 @@ yarn global add mint
|
||||
|
||||
```
|
||||
mint dev
|
||||
```
|
||||
```
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: Slack Integration (Beta)
|
||||
title: Slack Integration - Coming soon...
|
||||
description: This guide walks you through installing the OpenHands Slack app.
|
||||
---
|
||||
|
||||
<Warning>This integration is not live yet, but will be available soon.</Warning>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You are a slack workspace admin
|
||||
|
||||
@@ -46,7 +46,7 @@ poetry run python -m openhands.cli.main
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -55,7 +55,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ You can use the Settings page at any time to:
|
||||
- [Configure MCP servers](/usage/mcp).
|
||||
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
|
||||
- Set application settings like your preferred language, notifications and other preferences.
|
||||
- [Manage custom secrets](/usage/how-to/gui-mode#secrets-management).
|
||||
- Generate custom secrets.
|
||||
|
||||
#### GitHub Setup
|
||||
|
||||
@@ -122,36 +122,6 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
#### Secrets Management
|
||||
|
||||
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment variables in the agent's runtime environment.
|
||||
|
||||
1. **Accessing the Secrets Manager**:
|
||||
- In the Settings page, navigate to the `Secrets` tab.
|
||||
- You'll see a list of all your existing custom secrets (if any).
|
||||
|
||||
2. **Adding a New Secret**:
|
||||
- Click the `Add New Secret` button.
|
||||
- Fill in the following fields:
|
||||
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
|
||||
- **Value**: The sensitive information you want to store.
|
||||
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
|
||||
- Click `Add Secret` to save.
|
||||
|
||||
3. **Editing a Secret**:
|
||||
- Click the `Edit` button next to the secret you want to modify.
|
||||
- You can update the name and description of the secret.
|
||||
- Note: For security reasons, you cannot view or edit the value of an existing secret. If you need to change the value, delete the secret and create a new one.
|
||||
|
||||
4. **Deleting a Secret**:
|
||||
- Click the `Delete` button next to the secret you want to remove.
|
||||
- Confirm the deletion when prompted.
|
||||
|
||||
5. **Using Secrets in the Agent**:
|
||||
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
|
||||
- You can access them in your code using standard environment variable access methods (e.g., `os.environ['SECRET_NAME']` in Python).
|
||||
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as `process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.
|
||||
|
||||
#### Advanced Settings
|
||||
|
||||
The `Advanced` settings allows configuration of additional LLM settings. Inside the Settings page, under the `LLM` tab,
|
||||
@@ -184,7 +154,7 @@ is loaded. Typically these include:
|
||||
## Tips for Effective Use
|
||||
|
||||
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
|
||||
- Use one of the recommended models, as described in the [LLMs section](/usage/llms/llms).
|
||||
- Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
|
||||
|
||||
## Other Ways to Run Openhands
|
||||
- [Run OpenHands in a scriptable headless mode.](/usage/how-to/headless-mode)
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: OpenHands uses LiteLLM to make calls to Google's chat models. You c
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
|
||||
- `LLM Provider` to `Gemini`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
|
||||
(e.g. gemini/<model-name> like `gemini/gemini-2.0-flash`).
|
||||
- `API Key` to your Gemini API key
|
||||
|
||||
@@ -26,5 +26,5 @@ VERTEXAI_LOCATION="<your-gcp-location>"
|
||||
Then set the following in the OpenHands UI through the Settings under the `LLM` tab:
|
||||
- `LLM Provider` to `VertexAI`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
|
||||
(e.g. vertex_ai/<model-name>).
|
||||
|
||||
@@ -8,7 +8,7 @@ description: OpenHands uses LiteLLM to make calls to chat models on Groq. You ca
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
|
||||
- `LLM Provider` to `Groq`
|
||||
- `LLM Model` to the model you will be using. [Visit here to see the list of
|
||||
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list,
|
||||
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list,
|
||||
enable `Advanced` options, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ To use LiteLLM proxy with OpenHands, you need to:
|
||||
|
||||
## Supported Models
|
||||
|
||||
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy
|
||||
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy
|
||||
is configured to handle.
|
||||
|
||||
Refer to your LiteLLM proxy configuration for the list of available models and their names.
|
||||
|
||||
@@ -54,25 +54,25 @@ Check [the installation guide](/usage/local-setup) to make sure you have all the
|
||||
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
|
||||
export LMSTUDIO_URL="http://host.docker.internal:1234" # <- Replace this with the port from LMStudio
|
||||
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
|
||||
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
```
|
||||
|
||||
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -9,6 +9,6 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
* `LLM Provider` to `OpenRouter`
|
||||
* `LLM Model` to the model you will be using.
|
||||
[Visit here to see a full list of OpenRouter models](https://openrouter.ai/models).
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in
|
||||
If the model is not in the list, enable `Advanced` options, and enter it in
|
||||
`Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`).
|
||||
* `API Key` to your OpenRouter API key.
|
||||
|
||||
@@ -10,7 +10,6 @@ description: Getting started with running OpenHands on your own.
|
||||
- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements)
|
||||
- Linux
|
||||
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
|
||||
- Windows without WSL (see [Windows Without WSL Guide](/usage/windows-without-wsl))
|
||||
|
||||
A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.
|
||||
|
||||
@@ -56,10 +55,6 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
The docker command below to start the app must be run inside the WSL terminal.
|
||||
</Note>
|
||||
|
||||
**Alternative: Windows without WSL**
|
||||
|
||||
If you prefer to run OpenHands on Windows without WSL or Docker, see our [Windows Without WSL Guide](/usage/windows-without-wsl).
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -67,17 +62,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.42-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.43
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.42
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Organizations and users can define microagents that apply to all re
|
||||
|
||||
## Usage
|
||||
|
||||
These microagents can be [any type of microagent](./microagents-overview#microagent-types) and will be loaded
|
||||
These microagents can be [any type of microagent](./microagents-overview#microagent-types) and will be loaded
|
||||
accordingly. However, they are applied to all repositories belonging to the organization or user.
|
||||
|
||||
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
|
||||
|
||||
@@ -15,7 +15,7 @@ Before using the Local Runtime, ensure that:
|
||||
1. You can run OpenHands using the [Development workflow](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
2. For Linux and Mac, tmux is available on your system.
|
||||
3. For Windows, PowerShell is available on your system.
|
||||
- Only [CLI mode](../how-to/cli-mode) and [headless mode](../how-to/headless-mode) are supported in Windows with Local Runtime.
|
||||
- Only [CLI mode](../how-to/cli-mode) and [headless mode](../how-to/headless-mode) are supported in Windows with Local Runtime.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
---
|
||||
title: Windows Without WSL
|
||||
description: Running OpenHands GUI on Windows without using WSL or Docker
|
||||
---
|
||||
|
||||
# Running OpenHands GUI on Windows Without WSL
|
||||
|
||||
This guide provides step-by-step instructions for running OpenHands on a Windows machine without using WSL or Docker.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Windows 10/11** - A modern Windows operating system
|
||||
2. **PowerShell 7+** - While Windows PowerShell comes pre-installed on Windows 10/11, PowerShell 7+ is strongly recommended to avoid compatibility issues (see Troubleshooting section for "System.Management.Automation" errors)
|
||||
3. **.NET Core Runtime** - Required for the PowerShell integration via pythonnet
|
||||
4. **Python 3.12 or 3.13** - Python 3.12 or 3.13 is required (Python 3.14 is not supported due to pythonnet compatibility)
|
||||
5. **Git** - For cloning the repository and version control
|
||||
6. **Node.js and npm** - For running the frontend
|
||||
|
||||
## Step 1: Install Required Software
|
||||
|
||||
1. **Install Python 3.12 or 3.13**
|
||||
- Download Python 3.12.x or 3.13.x from [python.org](https://www.python.org/downloads/)
|
||||
- During installation, check "Add Python to PATH"
|
||||
- Verify installation by opening PowerShell and running:
|
||||
```powershell
|
||||
python --version
|
||||
```
|
||||
|
||||
2. **Install PowerShell 7**
|
||||
- Download and install PowerShell 7 from the [official PowerShell GitHub repository](https://github.com/PowerShell/PowerShell/releases)
|
||||
- Choose the MSI installer appropriate for your system (x64 for most modern computers)
|
||||
- Run the installer with default options
|
||||
- Verify installation by opening a new terminal and running:
|
||||
```powershell
|
||||
pwsh --version
|
||||
```
|
||||
- Using PowerShell 7 (pwsh) instead of Windows PowerShell will help avoid "System.Management.Automation" errors
|
||||
|
||||
3. **Install .NET Core Runtime**
|
||||
- Download and install the .NET Core Runtime from [Microsoft's .NET download page](https://dotnet.microsoft.com/download)
|
||||
- Choose the latest .NET Core Runtime (not SDK)
|
||||
- Verify installation by opening PowerShell and running:
|
||||
```powershell
|
||||
dotnet --info
|
||||
```
|
||||
- This step is required for the PowerShell integration via pythonnet. Without it, OpenHands will fall back to a more limited PowerShell implementation.
|
||||
|
||||
4. **Install Git**
|
||||
- Download Git from [git-scm.com](https://git-scm.com/download/win)
|
||||
- Use default installation options
|
||||
- Verify installation:
|
||||
```powershell
|
||||
git --version
|
||||
```
|
||||
|
||||
5. **Install Node.js and npm**
|
||||
- Download Node.js from [nodejs.org](https://nodejs.org/) (LTS version recommended)
|
||||
- During installation, accept the default options which will install npm as well
|
||||
- Verify installation:
|
||||
```powershell
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
6. **Install Poetry**
|
||||
- Open PowerShell as Administrator and run:
|
||||
```powershell
|
||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
||||
```
|
||||
- Add Poetry to your PATH:
|
||||
```powershell
|
||||
$env:Path += ";$env:APPDATA\Python\Scripts"
|
||||
```
|
||||
- Verify installation:
|
||||
```powershell
|
||||
poetry --version
|
||||
```
|
||||
|
||||
## Step 2: Clone and Set Up OpenHands
|
||||
|
||||
1. **Clone the Repository**
|
||||
```powershell
|
||||
git clone https://github.com/All-Hands-AI/OpenHands.git
|
||||
cd OpenHands
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
```powershell
|
||||
poetry install
|
||||
```
|
||||
|
||||
This will install all required dependencies, including:
|
||||
- pythonnet - Required for Windows PowerShell integration
|
||||
- All other OpenHands dependencies
|
||||
|
||||
## Step 3: Run OpenHands
|
||||
|
||||
1. **Build the Frontend**
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
```
|
||||
|
||||
This will build the frontend files that the backend will serve.
|
||||
|
||||
2. **Start the Backend**
|
||||
```powershell
|
||||
# Make sure to use PowerShell 7 (pwsh) instead of Windows PowerShell
|
||||
pwsh
|
||||
$env:RUNTIME="local"; poetry run uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000 --reload --reload-exclude "./workspace"
|
||||
```
|
||||
|
||||
This will start the OpenHands app using the local runtime with PowerShell integration, available at `localhost:3000`.
|
||||
|
||||
> **Note**: If you encounter a `RuntimeError: Directory './frontend/build' does not exist` error, make sure you've built the frontend first using the command above.
|
||||
|
||||
> **Important**: Using PowerShell 7 (pwsh) instead of Windows PowerShell is recommended to avoid "System.Management.Automation" errors. If you encounter this error, see the Troubleshooting section below.
|
||||
|
||||
3. **Alternatively, Run the Frontend in Development Mode (in a separate PowerShell window)**
|
||||
```powershell
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Access the OpenHands GUI**
|
||||
|
||||
Open your browser and navigate to:
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
> **Note**: If you're running the frontend in development mode (using `npm run dev`), use port 3001 instead: `http://localhost:3001`
|
||||
|
||||
## Limitations on Windows
|
||||
|
||||
When running OpenHands on Windows without WSL or Docker, be aware of the following limitations:
|
||||
|
||||
1. **Browser Tool Not Supported**: The browser tool is not currently supported on Windows.
|
||||
|
||||
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. If .NET Core is not available, OpenHands will automatically fall back to a more limited PowerShell implementation with reduced functionality.
|
||||
|
||||
3. **Interactive Shell Commands**: Some interactive shell commands may not work as expected. The PowerShell session implementation has limitations compared to the bash session used on Linux/macOS.
|
||||
|
||||
4. **Path Handling**: Windows uses backslashes (`\`) in paths, which may require adjustments when working with code examples designed for Unix-like systems.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "System.Management.Automation" Not Found Error
|
||||
|
||||
If you encounter an error message stating that "System.Management.Automation" was not found, this typically indicates that you have a minimal version of PowerShell installed or that the .NET components required for PowerShell integration are missing.
|
||||
|
||||
> **IMPORTANT**: This error is most commonly caused by using the built-in Windows PowerShell (powershell.exe) instead of PowerShell 7 (pwsh.exe). Even if you installed PowerShell 7 during the prerequisites, you may still be using the older Windows PowerShell by default.
|
||||
|
||||
To resolve this issue:
|
||||
|
||||
1. **Install the latest version of PowerShell 7** from the official Microsoft repository:
|
||||
- Visit [https://github.com/PowerShell/PowerShell/releases](https://github.com/PowerShell/PowerShell/releases)
|
||||
- Download and install the latest MSI package for your system architecture (x64 for most systems)
|
||||
- During installation, ensure you select the following options:
|
||||
- "Add PowerShell to PATH environment variable"
|
||||
- "Register Windows PowerShell 7 as the default shell"
|
||||
- "Enable PowerShell remoting"
|
||||
- The installer will place PowerShell 7 in `C:\Program Files\PowerShell\7` by default
|
||||
|
||||
2. **Restart your terminal or command prompt** to ensure the new PowerShell is available
|
||||
|
||||
3. **Verify the installation** by running:
|
||||
```powershell
|
||||
pwsh --version
|
||||
```
|
||||
|
||||
You should see output indicating PowerShell 7.x.x
|
||||
|
||||
4. **Run OpenHands using PowerShell 7** instead of Windows PowerShell:
|
||||
```powershell
|
||||
pwsh
|
||||
cd path\to\openhands
|
||||
$env:RUNTIME="local"; poetry run uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000 --reload --reload-exclude "./workspace"
|
||||
```
|
||||
|
||||
> **Note**: Make sure you're explicitly using `pwsh` (PowerShell 7) and not `powershell` (Windows PowerShell). The command prompt or terminal title should say "PowerShell 7" rather than just "Windows PowerShell".
|
||||
|
||||
5. **If the issue persists**, ensure that you have the .NET Runtime installed:
|
||||
- Download and install the latest .NET Runtime from [Microsoft's .NET download page](https://dotnet.microsoft.com/download)
|
||||
- Choose ".NET Runtime" (not SDK) version 6.0 or later
|
||||
- After installation, verify it's properly installed by running:
|
||||
```powershell
|
||||
dotnet --info
|
||||
```
|
||||
- Restart your computer after installation
|
||||
- Try running OpenHands again
|
||||
|
||||
6. **Ensure that the .NET Framework is properly installed** on your system:
|
||||
- Go to Control Panel > Programs > Programs and Features > Turn Windows features on or off
|
||||
- Make sure ".NET Framework 4.8 Advanced Services" is enabled
|
||||
- Click OK and restart if prompted
|
||||
|
||||
This error occurs because OpenHands uses the pythonnet package to interact with PowerShell, which requires the System.Management.Automation assembly from the .NET framework. A minimal PowerShell installation or older Windows PowerShell (rather than PowerShell 7+) might not include all the necessary components for this integration.
|
||||
@@ -1,4 +1,4 @@
|
||||
TASK_INSTRUECTION = """
|
||||
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:
|
||||
@@ -66,4 +66,4 @@ FAKE_USER_MSG_FOR_LOC = (
|
||||
'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'
|
||||
)
|
||||
)
|
||||
@@ -1,65 +0,0 @@
|
||||
<uploaded_files>
|
||||
/workspace/{{ workspace_dir_name }}
|
||||
</uploaded_files>
|
||||
|
||||
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{{ instance.problem_statement }}
|
||||
</issue_description>
|
||||
|
||||
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
|
||||
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
|
||||
Phase 1. READING: read the problem and reword it in clearer terms
|
||||
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
|
||||
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
|
||||
1.3 Explain the problem in clear terms.
|
||||
1.4 Enumerate the steps to reproduce the problem.
|
||||
1.5 Hightlight any best practices to take into account when testing and fixing the issue
|
||||
|
||||
Phase 2. RUNNING: install and run the tests on the repository
|
||||
2.1 Follow the readme
|
||||
2.2 Install the environment and anything needed
|
||||
2.2 Iterate and figure out how to run the tests
|
||||
|
||||
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
|
||||
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
|
||||
3.2 Identify all files related to the problem statement.
|
||||
3.3 Propose the methods and files to fix the issue and explain why.
|
||||
3.4 From the possible file locations, select the most likely location to fix the issue.
|
||||
|
||||
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
|
||||
4.1 Look at existing test files in the repository to understand the test format/structure.
|
||||
4.2 Create a minimal reproduction script that reproduces the located issue.
|
||||
4.3 Run the reproduction script to confirm you are reproducing the issue.
|
||||
4.4 Adjust the reproduction script as necessary.
|
||||
|
||||
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
|
||||
5.1 State clearly what the problem is.
|
||||
5.2 State clearly where the problem is located.
|
||||
5.3 State clearly how the test reproduces the issue.
|
||||
5.4 State clearly the best practices to take into account in the fix.
|
||||
5.5 State clearly how to fix the problem.
|
||||
|
||||
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
6.1 Make minimal, focused changes to fix the issue.
|
||||
|
||||
Phase 7. VERIFICATION: Test your implementation thoroughly.
|
||||
7.1 Run your reproduction script to verify the fix works.
|
||||
7.2 Add edge cases to your test script to ensure comprehensive coverage.
|
||||
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
|
||||
|
||||
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {{ instance.base_commit }}.
|
||||
8.1 Ensure you've fully addressed all requirements.
|
||||
8.2 Run any tests in the repository related to:
|
||||
8.2.1 The issue you are fixing
|
||||
8.2.2 The files you modified
|
||||
8.2.3 The functions you changed
|
||||
8.3 If any tests fail, revise your implementation until all tests pass
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
@@ -1,65 +0,0 @@
|
||||
<uploaded_files>
|
||||
/workspace/{{ workspace_dir_name }}
|
||||
</uploaded_files>
|
||||
|
||||
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{{ instance.problem_statement }}
|
||||
</issue_description>
|
||||
|
||||
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
|
||||
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
|
||||
Phase 1. READING: read the problem and reword it in clearer terms
|
||||
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
|
||||
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
|
||||
1.3 Explain the problem in clear terms.
|
||||
1.4 Enumerate the steps to reproduce the problem.
|
||||
1.5 Hightlight any best practices to take into account when testing and fixing the issue
|
||||
|
||||
Phase 2. RUNNING: install and run the tests on the repository
|
||||
2.1 Follow the readme
|
||||
2.2 Install the environment and anything needed
|
||||
2.2 Iterate and figure out how to run the tests
|
||||
|
||||
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
|
||||
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
|
||||
3.2 Identify all files related to the problem statement.
|
||||
3.3 Propose the methods and files to fix the issue and explain why.
|
||||
3.4 From the possible file locations, select the most likely location to fix the issue.
|
||||
|
||||
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
|
||||
4.1 Look at existing test files in the repository to understand the test format/structure.
|
||||
4.2 Create a minimal reproduction script that reproduces the located issue.
|
||||
4.3 Run the reproduction script to confirm you are reproducing the issue.
|
||||
4.4 Adjust the reproduction script as necessary.
|
||||
|
||||
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
|
||||
5.1 State clearly what the problem is.
|
||||
5.2 State clearly where the problem is located.
|
||||
5.3 State clearly how the test reproduces the issue.
|
||||
5.4 State clearly the best practices to take into account in the fix.
|
||||
5.5 State clearly how to fix the problem.
|
||||
|
||||
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
6.1 Make minimal, focused changes to fix the issue.
|
||||
|
||||
Phase 7. VERIFICATION: Test your implementation thoroughly.
|
||||
7.1 Run your reproduction script to verify the fix works.
|
||||
7.2 Add edge cases to your test script to ensure comprehensive coverage.
|
||||
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
|
||||
|
||||
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {{ instance.base_commit }}.
|
||||
8.1 Ensure you've fully addressed all requirements.
|
||||
8.2 Run any tests in the repository related to:
|
||||
8.2.1 The issue you are fixing
|
||||
8.2.2 The files you modified
|
||||
8.2.3 The functions you changed
|
||||
8.3 If any tests fail, revise your implementation until all tests pass
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Task: Fix Issue in Python Repository
|
||||
|
||||
## Repository Context
|
||||
You are provided with a Python code repository that contains an issue requiring your attention. The repository is located in a sandboxed environment, and you have access to the codebase to implement the necessary changes.
|
||||
The code repository is located at: `/workspace/{{ workspace_dir_name }}`
|
||||
(This path is provided for context; use file system tools to confirm paths before access).
|
||||
|
||||
## Goal
|
||||
Your goal is to fix the issue described in the **Issue Description** section below. Implement the necessary changes to **non-test files only** within the repository, ensuring that **all relevant tests pass** after your changes.
|
||||
|
||||
## Key Requirements & Constraints
|
||||
|
||||
1. **Understand the problem** very well: it is a bug report, and you know humans don't always write good descriptions. Explore the codebase to understand the related code and the problem in depth. It is possible that the solution needs to be a bit more extensive than just the stated text. Don't exagerate though: don't do unrelated refactoring, but also don't interpret the description too strictly.
|
||||
2. **Focus on the issues:** Implement the fix focusing on non-test files related to the issue.
|
||||
2. **Environment Ready:** The Python environment is pre-configured with all dependencies. Do not install packages.
|
||||
3. **Mandatory Testing Procedure:**
|
||||
* **Create Test to Reproduce the Issue:** *Before* implementing any fix, you MUST create a *new test* (separate from existing tests) that specifically reproduces the issue.
|
||||
* Take existing tests as example to understand the testing format/structure.
|
||||
* Enhance this test with edge cases.
|
||||
* Run this test to confirm reproduction.
|
||||
* **Verify Fix:** After implementing the fix, run your test again to verify the issue is resolved.
|
||||
* **Identify ALL Relevant Tests:** You MUST perform a **dedicated search and analysis** to identify **all** existing unit tests potentially affected by your changes. This includes:
|
||||
* Tests in the same module/directory as the changed files (e.g., `tests/` subdirectories).
|
||||
* Tests explicitly importing or using the modified code/classes/functions.
|
||||
* Tests mentioned in the issue description or related documentation.
|
||||
* Tests covering functionalities that *depend on* the modified code (analyze callers/dependencies if necessary).
|
||||
**If you cannot confidently identify a specific subset, you MUST identify and plan to run the entire test suite for the modified application or module(s). State your identified test scope clearly.**
|
||||
* **Run Identified Relevant Tests:** You MUST execute the **complete set** of relevant existing unit tests you identified in the previous step. Ensure you are running the *correct and comprehensive set* of tests. You MUST NOT modify these existing tests.
|
||||
* **Final Check & Verification:** Before finishing, ensure **all** identified relevant existing tests pass. **Explicitly confirm that you have considered potential omissions in your test selection and believe the executed tests comprehensively cover the impact of your changes.** Failing to identify and run the *complete* relevant set constitutes a failure. If any identified tests fail, revise your fix. Passing all relevant tests is the primary measure of success.
|
||||
4. **Defensive Programming:** Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
|
||||
5. **Final Review:** Compare your solution against the original issue and the base commit ({{ instance.base_commit }}) to ensure completeness and test passage.
|
||||
|
||||
## General Workflow Guidance
|
||||
|
||||
* Prioritize understanding the problem, exploring the code, planning your fix, implementing it carefully using the required diff format, and **thoroughly testing** according to the **Mandatory Testing Procedure**.
|
||||
* Consider trade-offs between different solutions. The goal is a **robust change that makes the relevant tests pass.** Quality, correctness, and reliability are key.
|
||||
* Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
|
||||
|
||||
* IMPORTANT: Your solution will be tested by additional hidden tests, so do not assume the task is complete just because visible tests pass! Refine the solution until you are confident that it is robust and comprehensive according to the **Defensive Programming** requirement.
|
||||
|
||||
## Final Note
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
|
||||
## Issue Description
|
||||
{{ instance.problem_statement }}
|
||||
@@ -1,80 +0,0 @@
|
||||
You will be tasked to fix an issue from an open-source repository.
|
||||
|
||||
Your thinking should be thorough and so it's fine if it's very long. You can think step by step before and after each action you decide to take.
|
||||
|
||||
You MUST iterate and keep going until the problem is solved.
|
||||
|
||||
You already have everything you need to solve this problem in the /workspace/{{ workspace_dir_name }} folder, even without internet connection. I want you to fully solve this autonomously before coming back to me.
|
||||
|
||||
Only terminate your turn when you are sure that the problem is solved. Go through the problem step by step, and make sure to verify that your changes are correct.
|
||||
NEVER end your turn without having solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
|
||||
|
||||
THE PROBLEM CAN DEFINITELY BE SOLVED WITHOUT THE INTERNET.
|
||||
|
||||
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect. If not, continue working on it.
|
||||
At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||
|
||||
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||
|
||||
# Workflow
|
||||
|
||||
## High-Level Problem Solving Strategy
|
||||
|
||||
1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
|
||||
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||
3. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps.
|
||||
4. Implement the fix incrementally. Make small, testable code changes.
|
||||
5. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||
6. Test frequently. Run tests after each change to verify correctness.
|
||||
7. Iterate until the root cause is fixed and all tests pass.
|
||||
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness,
|
||||
and remember there are hidden tests that must also pass before the solution is truly complete.
|
||||
|
||||
Refer to the detailed sections below for more information on each step.
|
||||
|
||||
## 1. Deeply Understand the Problem
|
||||
Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
|
||||
## 2. Codebase Investigation
|
||||
- Explore relevant files and directories.
|
||||
- Search for key functions, classes, or variables related to the issue.
|
||||
- Read and understand relevant code snippets.
|
||||
- Identify the root cause of the problem.
|
||||
- Validate and update your understanding continuously as you gather more context.
|
||||
|
||||
## 3. Develop a Detailed Plan
|
||||
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
|
||||
- Break down the fix into small, incremental changes.
|
||||
|
||||
## 4. Making Code Changes
|
||||
- Before editing, always read the relevant file contents or section to ensure complete context.
|
||||
- If a patch is not applied correctly, attempt to reapply it.
|
||||
- Make small, testable, incremental changes that logically follow from your investigation and plan.
|
||||
|
||||
## 5. Debugging
|
||||
- Make code changes only if you have high confidence they can solve the problem
|
||||
- When debugging, try to determine the root cause rather than addressing symptoms
|
||||
- Debug for as long as needed to identify the root cause and identify a fix
|
||||
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
|
||||
- To test hypotheses, you can also add test statements or functions
|
||||
- Revisit your assumptions if unexpected behavior occurs.
|
||||
|
||||
## 6. Testing
|
||||
- Run tests frequently using `python3 run_tests.py` (or equivalent).
|
||||
- After each change, verify correctness by running relevant tests.
|
||||
- If tests fail, analyze failures and revise your patch.
|
||||
- Write additional tests if needed to capture important behaviors or edge cases.
|
||||
- Ensure all tests pass before finalizing.
|
||||
|
||||
## 7. Final Verification
|
||||
- Confirm the root cause is fixed.
|
||||
- Review your solution for logic correctness and robustness.
|
||||
- Iterate until you are extremely confident the fix is complete and all tests pass.
|
||||
|
||||
## 8. Final Reflection and Additional Testing
|
||||
- Reflect carefully on the original intent of the user and the problem statement.
|
||||
- Think about potential edge cases or scenarios that may not be covered by existing tests.
|
||||
- Write additional tests that would need to pass to fully validate the correctness of your solution.
|
||||
- Run these new tests and ensure they all pass.
|
||||
- Be aware that there are additional hidden tests that must also pass for the solution to be successful.
|
||||
- Do not assume the task is complete just because the visible tests pass; continue refining until you are confident the fix is robust and comprehensive.
|
||||
@@ -1,19 +0,0 @@
|
||||
<uploaded_files>
|
||||
/workspace/{{ workspace_dir_name }}
|
||||
</uploaded_files>
|
||||
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{{ instance.problem_statement }}
|
||||
</issue_description>
|
||||
|
||||
|
||||
Can you help me implement the necessary changes to the repository to test whether the issue in <issue_description> was resolved?
|
||||
I will take care of all changes to any of the non-test files. This means you DON'T have to modify the actual logic and ONLY have to update test logic and tests!
|
||||
Your task is to make the minimal changes to tests files in the /workspace directory to reproduce the issue in the <issue_description>, i.e., such that the generated tests fail in the current state (where the issue is unresolved) and pass when the issue will be resolved.
|
||||
Follow these steps to reproduce the issue:
|
||||
1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.
|
||||
2. Create a script `reproduction.py` to reproduce the error and execute it with `python reproduction.py` using the BashTool, to confirm the error
|
||||
3. Edit the sourcecode of the repo to integrate your reproduction script into the test framework
|
||||
4. Run the test framework and make sure your tests fail! Only submit FAILING tests! Never submit passing tests.
|
||||
{{ test_instructions }}Your thinking should be thorough and so it's fine if it's very long.
|
||||
@@ -8,7 +8,6 @@ from typing import Any, Literal
|
||||
import pandas as pd
|
||||
import toml
|
||||
from datasets import load_dataset
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.swe_bench.binary_patch_utils import (
|
||||
@@ -43,7 +42,7 @@ from openhands.core.config import (
|
||||
AgentConfig,
|
||||
OpenHandsConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
get_parser
|
||||
)
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.utils import get_condenser_config_arg
|
||||
@@ -79,50 +78,101 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
mode = metadata.details['mode']
|
||||
llm_model = metadata.llm_config.model
|
||||
|
||||
# Determine the template file based on mode and LLM
|
||||
if mode.startswith('swt'):
|
||||
template_name = 'swt.j2'
|
||||
elif mode == 'swe':
|
||||
if 'claude' in llm_model:
|
||||
template_name = 'swe_claude.j2'
|
||||
elif 'gemini' in llm_model:
|
||||
template_name = 'swe_gemini.j2'
|
||||
elif 'gpt-4.1' in llm_model:
|
||||
template_name = 'swe_gpt4.j2'
|
||||
else:
|
||||
template_name = (
|
||||
'swe_default.j2' # Default for 'swe' mode (regular swe-bench)
|
||||
)
|
||||
else:
|
||||
# Fallback or error handling if mode is unexpected
|
||||
logger.error(f'Unexpected evaluation mode: {mode}. Falling back to default.')
|
||||
template_name = 'swe_default.j2'
|
||||
|
||||
# Set up Jinja2 environment
|
||||
# Assuming templates are in 'evaluation/benchmarks/swe_bench/prompts' relative to this script
|
||||
prompts_dir = os.path.join(os.path.dirname(__file__), 'prompts')
|
||||
env = Environment(loader=FileSystemLoader(prompts_dir))
|
||||
template = env.get_template(template_name)
|
||||
|
||||
# Prepare context for rendering
|
||||
context = {
|
||||
'instance': instance,
|
||||
'workspace_dir_name': workspace_dir_name,
|
||||
'metadata': metadata, # Pass metadata if needed in templates
|
||||
}
|
||||
|
||||
# Add specific context for swt-ci mode if needed
|
||||
if mode == 'swt-ci':
|
||||
context['test_instructions'] = (
|
||||
test_instructions = (
|
||||
f'The following command can be used to run the tests: `{list(MAP_REPO_TO_TEST_FRAMEWORK_VERBOSE[instance.repo].values())[0]}`. Make sure they fail in the expected way.\n'
|
||||
if mode.endswith('ci')
|
||||
else ''
|
||||
)
|
||||
else:
|
||||
context['test_instructions'] = '' # Ensure it's defined for other modes
|
||||
instruction = f"""\
|
||||
<uploaded_files>
|
||||
/workspace/{workspace_dir_name}
|
||||
</uploaded_files>
|
||||
I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:
|
||||
|
||||
# Render the instruction
|
||||
instruction = template.render(context)
|
||||
<issue_description>
|
||||
{instance.problem_statement}
|
||||
</issue_description>
|
||||
|
||||
|
||||
Can you help me implement the necessary changes to the repository to test whether the issue in <issue_description> was resolved?
|
||||
I will take care of all changes to any of the non-test files. This means you DON'T have to modify the actual logic and ONLY have to update test logic and tests!
|
||||
Your task is to make the minimal changes to tests files in the /workspace directory to reproduce the issue in the <issue_description>, i.e., such that the generated tests fail in the current state (where the issue is unresolved) and pass when the issue will be resolved.
|
||||
Follow these steps to reproduce the issue:
|
||||
1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.
|
||||
2. Create a script `reproduction.py` to reproduce the error and execute it with `python reproduction.py` using the BashTool, to confirm the error
|
||||
3. Edit the sourcecode of the repo to integrate your reproduction script into the test framework
|
||||
4. Run the test framework and make sure your tests fail! Only submit FAILING tests! Never submit passing tests.
|
||||
{test_instructions}Your thinking should be thorough and so it's fine if it's very long.
|
||||
"""
|
||||
else:
|
||||
instruction = f"""
|
||||
<uploaded_files>
|
||||
/workspace/{workspace_dir_name}
|
||||
</uploaded_files>
|
||||
|
||||
I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{instance.problem_statement}
|
||||
</issue_description>
|
||||
|
||||
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
|
||||
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
|
||||
Phase 1. READING: read the problem and reword it in clearer terms
|
||||
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
|
||||
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
|
||||
1.3 Explain the problem in clear terms.
|
||||
1.4 Enumerate the steps to reproduce the problem.
|
||||
1.5 Hightlight any best practices to take into account when testing and fixing the issue
|
||||
|
||||
Phase 2. RUNNING: install and run the tests on the repository
|
||||
2.1 Follow the readme
|
||||
2.2 Install the environment and anything needed
|
||||
2.2 Iterate and figure out how to run the tests
|
||||
|
||||
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
|
||||
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
|
||||
3.2 Identify all files related to the problem statement.
|
||||
3.3 Propose the methods and files to fix the issue and explain why.
|
||||
3.4 From the possible file locations, select the most likely location to fix the issue.
|
||||
|
||||
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
|
||||
4.1 Look at existing test files in the repository to understand the test format/structure.
|
||||
4.2 Create a minimal reproduction script that reproduces the located issue.
|
||||
4.3 Run the reproduction script to confirm you are reproducing the issue.
|
||||
4.4 Adjust the reproduction script as necessary.
|
||||
|
||||
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
|
||||
5.1 State clearly what the problem is.
|
||||
5.2 State clearly where the problem is located.
|
||||
5.3 State clearly how the test reproduces the issue.
|
||||
5.4 State clearly the best practices to take into account in the fix.
|
||||
5.5 State clearly how to fix the problem.
|
||||
|
||||
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
6.1 Make minimal, focused changes to fix the issue.
|
||||
|
||||
Phase 7. VERIFICATION: Test your implementation thoroughly.
|
||||
7.1 Run your reproduction script to verify the fix works.
|
||||
7.2 Add edge cases to your test script to ensure comprehensive coverage.
|
||||
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
|
||||
|
||||
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance['base_commit']}.
|
||||
8.1 Ensure you've fully addressed all requirements.
|
||||
8.2 Run any tests in the repository related to:
|
||||
8.2.1 The issue you are fixing
|
||||
8.2.2 The files you modified
|
||||
8.2.3 The functions you changed
|
||||
8.3 If any tests fail, revise your implementation until all tests pass
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
"""
|
||||
|
||||
if RUN_WITH_BROWSING:
|
||||
instruction += (
|
||||
|
||||
@@ -921,7 +921,7 @@ SPECS_PYDICOM.update(
|
||||
|
||||
SPECS_HUMANEVAL = {k: {'python': '3.9', 'test_cmd': 'python'} for k in ['1.0']}
|
||||
|
||||
# Constants - Task Instance Installation Environment
|
||||
# Constants - Task Instance Instllation Environment
|
||||
MAP_REPO_VERSION_TO_SPECS: dict[str, dict[str, Any]] = {
|
||||
'astropy/astropy': SPECS_ASTROPY,
|
||||
'dbt-labs/dbt-core': SPECS_DBT_CORE,
|
||||
|
||||
@@ -539,7 +539,7 @@ if __name__ == '__main__':
|
||||
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 accuracy of results
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# VersiCode benchmark
|
||||
|
||||
This project is used to evaluate the performance of the model on VersiCode. It includes:
|
||||
|
||||
- data: the test data needed and the model outputs
|
||||
- inference_utils: inference scripts for ours tasks and models
|
||||
- metric: scripts for calculating various metric
|
||||
- output_processing: process the model output to facilitate the calculation of model metrics
|
||||
|
||||
# Details
|
||||
|
||||
1. **Prepare the environment**
|
||||
|
||||
```shell
|
||||
#create conda environment
|
||||
conda create -n VersiCode python==3.12
|
||||
|
||||
#install requirements
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Experiment Data**
|
||||
|
||||
To obtain the experimental data, please visit the Hugging Face link: https://huggingface.co/datasets/AstoneNg/VersiCode.
|
||||
Locate the files `VersiCode_block_completion.json` and `VersiCode_migration.json` under the `experiment_data` directory, and place them in the `/data/test_data directory` of this project.
|
||||
|
||||
|
||||
3. **Model inference**
|
||||
|
||||
```shell
|
||||
#cd inference_utils directory
|
||||
cd inference_utils
|
||||
|
||||
#The script file starting with 'test' is used to test the local model
|
||||
#The script file at the beginning of the API is used to test the API call model
|
||||
|
||||
#block level code completipn
|
||||
#Modify the 10th and 12th lines of code to specify the base URL and model name
|
||||
python api_test_block_completion.py
|
||||
#Modify the 30th line of code to specify the local model path
|
||||
python test_block.py
|
||||
|
||||
# code migration (migration order is 'old_to_new')
|
||||
#Modify the 10th and 12th lines of code to specify the base URL and model name
|
||||
python api_code_migration.py
|
||||
#Modify the 30th line of code to specify the local model path
|
||||
python test_migration.py
|
||||
```
|
||||
|
||||
4. **Process output**
|
||||
Process the output content of the model, remove redundant content, extract specified content for easy calculation of indicators.
|
||||
|
||||
```shell
|
||||
#cd output_processing
|
||||
cd output_processing
|
||||
|
||||
#Extract content from<start> and <end>
|
||||
#Modify the 8th and 9th lines of code to specify the model and task granularity
|
||||
python clear_ans.py
|
||||
|
||||
#In the block completion task and migration task, cdc@k The calculation of indicators needs to be targeted at key rows,
|
||||
#Modify lines 76 and 79 to specify the data path
|
||||
python choose_core_line_from_block_versicode.py
|
||||
python choose_core_line_from_migration_versicode.py
|
||||
```
|
||||
|
||||
5. **Metric**
|
||||
We have three metrics pass@k,em@k and cdc@k Due to our inability to automatically build a dynamic evaluation environment, we have not provided pass@k .
|
||||
|
||||
```shell
|
||||
#cd metric
|
||||
cd metric
|
||||
|
||||
#Modify lines 137-140 in migration task (compute_migration_cdc_score.py) or 143-145 in block and line completion task (compute_versicode_cdc_score.py and compute_versicode_em_score.py) of the code to specify the data path and calculate the k-value of the metric
|
||||
python compute_migration_cdc_score.py
|
||||
python compute_versicode_cdc_score.py
|
||||
python compute_versicode_em_score.py
|
||||
|
||||
#Notes
|
||||
#We found limitations in the ISM@k and PM@k metrics for evaluating code generation, so they are used only as reference in our experiments.
|
||||
#Modify lines 261-265 in block and line completion task of the code to specify the data path and calculate the k-value of the metric
|
||||
python compute_ism_pm_score.py
|
||||
```
|
||||
|
||||
# Citation
|
||||
|
||||
```
|
||||
@article{versicode,
|
||||
author={Tongtong Wu and Weigang Wu and Xingyu Wang and Kang Xu and Suyu Ma and Bo Jiang and Ping Yang and Zhenchang Xing and Yuan-Fang Li and Gholamreza Haffari},
|
||||
title = {VersiCode: Towards Version-controllable Code Generation},
|
||||
journal = {CoRR},
|
||||
volume = {abs/2406.07411},
|
||||
year = {2024},
|
||||
url = {https://arxiv.org/abs/2406.07411},
|
||||
}
|
||||
```
|
||||
|
||||
**Github url**: https://github.com/wutong8023/VersiCode
|
||||
|
||||
# Contributor
|
||||
|
||||
[Tongtong Wu](https://scholar.google.com/citations?hl=zh-CN&user=u1Qp8lUAAAAJ&view_op=list_works&sortby=pubdate), [Weigang Wu](https://scholar.google.com/citations?hl=zh-CN&user=UneIZo8AAAAJ), [Xingyu Wang](https://scholar.google.com/citations?hl=zh-CN&user=wqPJcxcAAAAJ), [Kang Xu](https://scholar.google.com/citations?hl=zh-CN&user=N1UUDi0AAAAJ), [Suyu Ma](https://scholar.google.com/citations?hl=zh-CN&user=NJHR1ukAAAAJ), [Bo Jiang](https://wutong8023.site/VersiCode/), [Ping Yang](https://scholar.google.com/citations?view_op=list_works&hl=en&hl=en&user=hrogvxoAAAAJ), [Zhenchang Xing](https://scholar.google.com/citations?hl=zh-CN&user=0vCxuH4AAAAJ), [Yuan-Fang Li](https://scholar.google.com/citations?hl=zh-CN&user=wufXO1kAAAAJ), [Gholamreza Haffari](https://scholar.google.com/citations?hl=zh-CN&user=Perjx5EAAAAJ)
|
||||
@@ -1,134 +0,0 @@
|
||||
"""
|
||||
GPT performs line level generation prediction and truncates overly long tokens
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import tiktoken
|
||||
from openai import OpenAI
|
||||
|
||||
max_tokens = 127000 # gpt3.5 is 16ktoken gpt4o is 128k
|
||||
model_name = ''
|
||||
|
||||
os.environ['OPENAI_API_KEY'] = ''
|
||||
client = OpenAI()
|
||||
|
||||
|
||||
def truncate_text(text, max_tokens):
|
||||
encoding = tiktoken.get_encoding('cl100k_base')
|
||||
disallowed_special = ()
|
||||
|
||||
tokens = encoding.encode(text, disallowed_special=disallowed_special)
|
||||
print(len(tokens))
|
||||
|
||||
if len(tokens) > max_tokens:
|
||||
tokens = tokens[:max_tokens]
|
||||
|
||||
truncated_text = encoding.decode(tokens)
|
||||
|
||||
return truncated_text
|
||||
|
||||
|
||||
def predict(content, model_name):
|
||||
response = client.chat.completions.create(
|
||||
model=model_name,
|
||||
messages=[{'role': 'user', 'content': content}],
|
||||
frequency_penalty=0.1,
|
||||
max_tokens=128,
|
||||
logit_bias=None,
|
||||
logprobs=None,
|
||||
n=6,
|
||||
presence_penalty=0.0,
|
||||
seed=None,
|
||||
stop=None,
|
||||
stream=False,
|
||||
temperature=0.8,
|
||||
top_p=0.95,
|
||||
)
|
||||
ans_list = []
|
||||
choices_list = response.choices
|
||||
for c in choices_list:
|
||||
content = c.message.content
|
||||
ans_list.append(content)
|
||||
final_ans = str(ans_list)
|
||||
return final_ans
|
||||
|
||||
|
||||
def bulid_prompt(description, old_version, old_code, new_version) -> str:
|
||||
"""
|
||||
build prompt
|
||||
:param version:
|
||||
:param description:
|
||||
:param masked_code:
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
prompt = f"""
|
||||
You are now a professional Python programming engineer. I will provide you with a code snippet and a description of its functionality,
|
||||
including the dependencies and versions used in the code. Then, I will provide the same dependencies but with a specified new version.
|
||||
Your task is to refactor the code using the methods provided by the specified new version and return the refactored code.
|
||||
Please note that you only need to return the refactored code and enclose it with <start> and <end>:
|
||||
###Functionality description of the code
|
||||
{description}
|
||||
###Dependency and old version
|
||||
{old_version}
|
||||
###Old version code
|
||||
{old_code}
|
||||
###Dependency and new version
|
||||
{new_version}
|
||||
###Refactored new code
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
json_path = '../data/test_data/VersiCode_migration.json'
|
||||
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_dict = lodict
|
||||
data_list = data_dict
|
||||
|
||||
|
||||
for data in data_list:
|
||||
if 'model_output' in data:
|
||||
print(
|
||||
f'the {data_list.index(data) + 1} has already been predicted, skipping this data!'
|
||||
)
|
||||
continue
|
||||
try:
|
||||
print(f'Predicting {data_list.index(data) + 1} ')
|
||||
old_version = data['dependency'] + data['old_version'] # package == x.x.x
|
||||
new_version = data['dependency'] + data['new_version'] # package == x.x.x
|
||||
description = data['description'] # 功能描述
|
||||
old_code = data['old_code'] # mask后的代码
|
||||
|
||||
instruction = bulid_prompt(description, old_version, old_code, new_version)
|
||||
truncated_text = truncate_text(instruction, max_tokens)
|
||||
prediction = predict(truncated_text, model_name)
|
||||
|
||||
data['model_output'] = prediction
|
||||
except Exception as e:
|
||||
print(f'error:{e}')
|
||||
print('save current data')
|
||||
save_folder_path = os.path.join(
|
||||
'../data/result_data/code_migration', model_name
|
||||
)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(data_dict, fw, indent=4, ensure_ascii=False)
|
||||
break
|
||||
|
||||
|
||||
save_folder_path = os.path.join('../data/result_data/code_migration', model_name)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(data_dict, fw, indent=4, ensure_ascii=False)
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
GPT performs line level generation prediction and truncates overly long tokens
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import tiktoken
|
||||
from openai import OpenAI
|
||||
|
||||
max_tokens = 127000 # gpt3.5 is 16ktoken gpt4o is 128k
|
||||
model_name = ''
|
||||
|
||||
os.environ['OPENAI_API_KEY'] = ''
|
||||
client = OpenAI()
|
||||
|
||||
|
||||
def truncate_text(text, max_tokens):
|
||||
encoding = tiktoken.get_encoding('cl100k_base')
|
||||
disallowed_special = ()
|
||||
|
||||
tokens = encoding.encode(text, disallowed_special=disallowed_special)
|
||||
print(len(tokens))
|
||||
|
||||
if len(tokens) > max_tokens:
|
||||
tokens = tokens[:max_tokens]
|
||||
|
||||
truncated_text = encoding.decode(tokens)
|
||||
|
||||
return truncated_text
|
||||
|
||||
|
||||
def predict(content, model_name):
|
||||
response = client.chat.completions.create(
|
||||
model=model_name,
|
||||
messages=[{'role': 'user', 'content': content}],
|
||||
frequency_penalty=0.1,
|
||||
max_tokens=128,
|
||||
logit_bias=None,
|
||||
logprobs=None,
|
||||
n=6,
|
||||
presence_penalty=0.0,
|
||||
seed=None,
|
||||
stop=None,
|
||||
stream=False,
|
||||
temperature=0.8,
|
||||
top_p=0.95,
|
||||
)
|
||||
ans_list = []
|
||||
choices_list = response.choices
|
||||
for c in choices_list:
|
||||
content = c.message.content
|
||||
ans_list.append(content)
|
||||
final_ans = str(ans_list)
|
||||
return final_ans
|
||||
|
||||
|
||||
def bulid_prompt(version, description) -> str:
|
||||
"""
|
||||
build prompt
|
||||
:param version:
|
||||
:param description:
|
||||
:param masked_code:
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
prompt = f"""
|
||||
You are a professional Python engineer, and I will provide functional descriptions and versions of specified dependency packages.
|
||||
You need to write code in Python to implement this feature based on the functional description and using the dependency package and version I specified.
|
||||
Please note that you only need to return the code that implements the function, and do not return any other content.
|
||||
Please use <start> and <end> to enclose the generated code. Here is an example:
|
||||
###Function Description:
|
||||
The function of this code is to print the results predicted by calling the model using vllm.
|
||||
###dependeny and version:
|
||||
vllm==0.3.3
|
||||
###response:
|
||||
<start>
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
print("Prompt,Generated text")
|
||||
<end>
|
||||
|
||||
###Function Description:
|
||||
{description}
|
||||
###dependeny and version:
|
||||
{version}
|
||||
###response:
|
||||
|
||||
|
||||
"""
|
||||
return prompt
|
||||
|
||||
|
||||
json_path = '../data/test_data/VersiCode_block_completion.json'
|
||||
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_dict = lodict
|
||||
data_list = data_dict
|
||||
|
||||
|
||||
for data in data_list:
|
||||
if 'model_output' in data:
|
||||
print(
|
||||
f'the {data_list.index(data) + 1} has already been predicted, skipping this data!'
|
||||
)
|
||||
continue
|
||||
try:
|
||||
print(f'Predicting {data_list.index(data) + 1} ')
|
||||
version = data['dependency'] + data['version'] # package == x.x.x
|
||||
description = data['description'] # func description
|
||||
|
||||
instruction = bulid_prompt(version, description)
|
||||
truncated_text = truncate_text(instruction, max_tokens)
|
||||
prediction = predict(truncated_text, model_name)
|
||||
|
||||
data['model_output'] = prediction
|
||||
except Exception as e:
|
||||
print(f'error:{e}')
|
||||
print('save current data')
|
||||
save_folder_path = os.path.join(
|
||||
'../data/result_data/block_completion', model_name
|
||||
)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(data_dict, fw, indent=4, ensure_ascii=False)
|
||||
break
|
||||
|
||||
|
||||
save_folder_path = os.path.join('../data/result_data/block_completion', model_name)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(data_dict, fw, indent=4, ensure_ascii=False)
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
block completion
|
||||
"""
|
||||
|
||||
import copy
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
import tiktoken
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
# os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
|
||||
|
||||
|
||||
def truncate_text(text, max_tokens):
|
||||
encoding = tiktoken.get_encoding('cl100k_base')
|
||||
disallowed_special = ()
|
||||
|
||||
tokens = encoding.encode(text, disallowed_special=disallowed_special)
|
||||
print(len(tokens))
|
||||
|
||||
if len(tokens) > max_tokens:
|
||||
tokens = tokens[:max_tokens]
|
||||
|
||||
truncated_text = encoding.decode(tokens)
|
||||
|
||||
return truncated_text
|
||||
|
||||
|
||||
model_list = ['/data2/base models/starcoder2-15b', '/data2/base models/CodeGemma-7B']
|
||||
|
||||
|
||||
def run_inference(model_name, origin_data_list):
|
||||
temp_data_list = copy.deepcopy(origin_data_list)
|
||||
test_list = []
|
||||
for data in temp_data_list:
|
||||
version = data['dependency'] + data['version'] # package == x.x.x
|
||||
description = data['description'] # func description
|
||||
|
||||
instruction = bulid_prompt(version, description)
|
||||
test_list.append(instruction)
|
||||
|
||||
sampling_params = SamplingParams(n=6, temperature=0.8, top_p=0.95, max_tokens=64)
|
||||
llm = LLM(
|
||||
model=model_name,
|
||||
tensor_parallel_size=4,
|
||||
gpu_memory_utilization=0.9,
|
||||
swap_space=20,
|
||||
)
|
||||
|
||||
outputs = llm.generate(test_list, sampling_params)
|
||||
for output in outputs:
|
||||
requests_id = int(output.request_id)
|
||||
temp_ans_list = []
|
||||
output_list = output.outputs
|
||||
for o in output_list:
|
||||
text = o.text
|
||||
temp_ans_list.append(text)
|
||||
|
||||
temp_data_list[requests_id]['model_output'] = str(temp_ans_list)
|
||||
|
||||
save_folder_path = os.path.join(
|
||||
'../data/result_data/block_completion', model_name.split('/')[-1]
|
||||
)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(temp_data_list, fw, indent=4, ensure_ascii=False)
|
||||
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
def bulid_prompt(version, description) -> str:
|
||||
"""
|
||||
build prompt
|
||||
:param version:
|
||||
:param description:
|
||||
:param masked_code:
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
prompt = f"""
|
||||
You are a professional Python engineer, and I will provide functional descriptions and versions of specified dependency packages.
|
||||
You need to write code in Python to implement this feature based on the functional description and using the dependency package and version I specified.
|
||||
Please note that you only need to return the code that implements the function, and do not return any other content.
|
||||
Please use <start> and <end> to enclose the generated code. Here is an example:
|
||||
###Function Description:
|
||||
The function of this code is to print the results predicted by calling the model using vllm.
|
||||
###dependeny and version:
|
||||
vllm==0.3.3
|
||||
###response:
|
||||
<start>
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
print("Prompt,Generated text")
|
||||
<end>
|
||||
|
||||
###Function Description:
|
||||
{description}
|
||||
###dependeny and version:
|
||||
{version}
|
||||
###response:
|
||||
|
||||
|
||||
"""
|
||||
return prompt
|
||||
|
||||
|
||||
json_path = '../data/test_data/VersiCode_block_completion.json'
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
|
||||
origin_data_list = lodict
|
||||
|
||||
for model_name in model_list:
|
||||
process = Process(target=run_inference, args=(model_name, origin_data_list))
|
||||
process.start()
|
||||
process.join()
|
||||
time.sleep(120)
|
||||
@@ -1,122 +0,0 @@
|
||||
"""
|
||||
code migration
|
||||
"""
|
||||
|
||||
import copy
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
import tiktoken
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
# os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
|
||||
|
||||
|
||||
def truncate_text(text, max_tokens):
|
||||
encoding = tiktoken.get_encoding('cl100k_base')
|
||||
disallowed_special = ()
|
||||
|
||||
tokens = encoding.encode(text, disallowed_special=disallowed_special)
|
||||
print(len(tokens))
|
||||
|
||||
if len(tokens) > max_tokens:
|
||||
tokens = tokens[:max_tokens]
|
||||
|
||||
truncated_text = encoding.decode(tokens)
|
||||
|
||||
return truncated_text
|
||||
|
||||
|
||||
model_list = ['/data2/base models/starcoder2-15b', '/data2/base models/CodeGemma-7B']
|
||||
|
||||
|
||||
def run_inference(model_name, origin_data_list):
|
||||
temp_data_list = copy.deepcopy(origin_data_list)
|
||||
test_list = []
|
||||
for data in temp_data_list:
|
||||
old_version = data['dependency'] + data['old_version'] # package == x.x.x
|
||||
new_version = data['dependency'] + data['new_version'] # package == x.x.x
|
||||
description = data['description'] # 功能描述
|
||||
old_code = data['old_code'] # mask后的代码
|
||||
|
||||
instruction = bulid_prompt(description, old_version, old_code, new_version)
|
||||
test_list.append(instruction)
|
||||
|
||||
sampling_params = SamplingParams(n=6, temperature=0.8, top_p=0.95, max_tokens=512)
|
||||
llm = LLM(
|
||||
model=model_name,
|
||||
tensor_parallel_size=4,
|
||||
gpu_memory_utilization=0.6,
|
||||
swap_space=40,
|
||||
)
|
||||
|
||||
outputs = llm.generate(test_list, sampling_params)
|
||||
for output in outputs:
|
||||
requests_id = int(output.request_id)
|
||||
temp_ans_list = []
|
||||
output_list = output.outputs
|
||||
for o in output_list:
|
||||
text = o.text
|
||||
temp_ans_list.append(text)
|
||||
|
||||
temp_data_list[requests_id]['model_output'] = str(temp_ans_list)
|
||||
|
||||
save_folder_path = os.path.join(
|
||||
'../data/result_data/code_migration', model_name.split('/')[-1]
|
||||
)
|
||||
if not os.path.exists(save_folder_path):
|
||||
os.makedirs(save_folder_path)
|
||||
|
||||
save_json_path = os.path.join(save_folder_path, json_path.split('/')[-1])
|
||||
|
||||
with open(save_json_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(temp_data_list, fw, indent=4, ensure_ascii=False)
|
||||
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
def bulid_prompt(description, old_version, old_code, new_version) -> str:
|
||||
"""
|
||||
build prompt
|
||||
:param version:
|
||||
:param description:
|
||||
:param masked_code:
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
prompt = f"""
|
||||
You are now a professional Python programming engineer. I will provide you with a code snippet and a description of its functionality,
|
||||
including the dependencies and versions used in the code. Then, I will provide the same dependencies but with a specified new version.
|
||||
Your task is to refactor the code using the methods provided by the specified new version and return the refactored code.
|
||||
Please note that you only need to return the refactored code and enclose it with <start> and <end>:
|
||||
###Functionality description of the code
|
||||
{description}
|
||||
###Dependency and old version
|
||||
{old_version}
|
||||
###Old version code
|
||||
{old_code}
|
||||
###Dependency and new version
|
||||
{new_version}
|
||||
###Refactored new code
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
json_path = '../data/test_data/VersiCode_migration.json'
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
|
||||
origin_data_list = lodict
|
||||
|
||||
for model_name in model_list:
|
||||
process = Process(target=run_inference, args=(model_name, origin_data_list))
|
||||
process.start()
|
||||
process.join()
|
||||
time.sleep(120)
|
||||
@@ -1,356 +0,0 @@
|
||||
"""
|
||||
评测block的预测能力
|
||||
1、判断是否包含正确的函数名
|
||||
2、判断是否合法
|
||||
3、计算ISM,和PM
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
|
||||
def is_code_valid(code):
|
||||
try:
|
||||
compile(code, '<string>', 'exec')
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def longest_common_prefix_between_lists_with_elements(list1, list2):
|
||||
"""
|
||||
计算两个字符串列表中元素的最长前缀匹配长度
|
||||
:param list1:
|
||||
:param list2:
|
||||
:return:
|
||||
"""
|
||||
max_prefix_length = 0
|
||||
max_prefix_elements = ()
|
||||
for str1 in list1:
|
||||
for str2 in list2:
|
||||
prefix_length = 0
|
||||
min_len = min(len(str1), len(str2))
|
||||
for i in range(min_len):
|
||||
if str1[i] == str2[i]:
|
||||
prefix_length += 1
|
||||
else:
|
||||
break
|
||||
if prefix_length > max_prefix_length:
|
||||
max_prefix_length = prefix_length
|
||||
max_prefix_elements = (str1, str2)
|
||||
return max_prefix_length, max_prefix_elements
|
||||
|
||||
|
||||
def get_token(ans_code: str, output_code: str):
|
||||
"""
|
||||
对代码进行词法分析,分解成标识符,返回两个标识符列表
|
||||
:param ans_code:
|
||||
:param output_code:
|
||||
:return:
|
||||
"""
|
||||
output_flag = True
|
||||
ans_flag = True
|
||||
try:
|
||||
tokens_ans = tokenize.tokenize(io.BytesIO(ans_code.encode('utf-8')).readline)
|
||||
except Exception:
|
||||
tokens_ans = ans_code.splitlines()
|
||||
ans_flag = False
|
||||
|
||||
try:
|
||||
tokens_output = tokenize.tokenize(
|
||||
io.BytesIO(output_code.encode('utf-8')).readline
|
||||
)
|
||||
except Exception:
|
||||
tokens_output = output_code.splitlines()
|
||||
output_flag = False
|
||||
|
||||
identifiers_ans = []
|
||||
identifiers_output = []
|
||||
if ans_flag:
|
||||
try:
|
||||
for token in tokens_ans:
|
||||
if token.type == tokenize.NAME:
|
||||
identifiers_ans.append(token.string)
|
||||
except Exception:
|
||||
identifiers_ans = tokens_ans
|
||||
else:
|
||||
identifiers_ans = tokens_ans
|
||||
|
||||
if output_flag:
|
||||
try:
|
||||
for to in tokens_output:
|
||||
if to.type == tokenize.NAME:
|
||||
identifiers_output.append(to.string)
|
||||
except Exception:
|
||||
identifiers_output = tokens_output
|
||||
else:
|
||||
identifiers_output = tokens_output
|
||||
|
||||
return identifiers_ans, identifiers_output
|
||||
|
||||
|
||||
def get_token_per_line(code: str):
|
||||
"""
|
||||
对每一行代码进行词法分析,记录每一行的标识符
|
||||
:param code: 代码字符串
|
||||
:return: 每一行的标识符列表组成的列表
|
||||
"""
|
||||
lines = code.split('\n') # 将代码按行分割成列表
|
||||
identifiers_per_line = [] # 用于存储每一行的标识符列表的列表
|
||||
|
||||
for line in lines:
|
||||
tokens = tokenize.tokenize(io.BytesIO(line.encode('utf-8')).readline)
|
||||
identifiers = []
|
||||
try:
|
||||
for token in tokens:
|
||||
if token.type == tokenize.NAME:
|
||||
identifiers.append(token.string)
|
||||
except Exception:
|
||||
identifiers = line.split(' ')
|
||||
identifiers_per_line.append(identifiers)
|
||||
|
||||
return identifiers_per_line
|
||||
|
||||
|
||||
def get_ISM(answer_code: str, model_output_list: list, asnwer_name: str) -> list:
|
||||
"""
|
||||
计算ISM,返回一个有序的得分列表
|
||||
:return:
|
||||
"""
|
||||
score_list = []
|
||||
for code in model_output_list:
|
||||
if '```python' in code:
|
||||
code = code.replace('```python', '')
|
||||
code = code.replace('```', '')
|
||||
if not re.search(rf'\b{re.escape(asnwer_name)}\b', code) or not is_code_valid(
|
||||
code
|
||||
):
|
||||
score_list.append(0)
|
||||
continue
|
||||
|
||||
# if asnwer_name not in code:
|
||||
# score_list.append(0)
|
||||
# continue
|
||||
|
||||
identifiers_ans, identifiers_output = get_token(answer_code, code)
|
||||
max_len, elements = longest_common_prefix_between_lists_with_elements(
|
||||
identifiers_ans, identifiers_output
|
||||
)
|
||||
if max_len != 0:
|
||||
base_element_len = max(len(elements[0]), len(elements[1]))
|
||||
temp_score = max_len / base_element_len
|
||||
score_list.append(temp_score)
|
||||
else:
|
||||
score_list.append(0)
|
||||
# base_element_len = max(len(elements[0]), len(elements[1]))
|
||||
# temp_score = max_len/base_element_len
|
||||
# score_list.append(temp_score)
|
||||
|
||||
score_list = sorted(score_list, reverse=True)
|
||||
return score_list
|
||||
|
||||
|
||||
def get_ISM_without_verification(
|
||||
answer_code: str, model_output_list: list, asnwer_name: str
|
||||
) -> list:
|
||||
"""
|
||||
计算ISM,返回一个有序的得分列表
|
||||
:return:
|
||||
"""
|
||||
score_list = []
|
||||
for code in model_output_list:
|
||||
if asnwer_name not in code:
|
||||
score_list.append(0)
|
||||
continue
|
||||
|
||||
# if asnwer_name not in code:
|
||||
# score_list.append(0)
|
||||
# continue
|
||||
|
||||
identifiers_ans, identifiers_output = get_token(answer_code, code)
|
||||
max_len, elements = longest_common_prefix_between_lists_with_elements(
|
||||
identifiers_ans, identifiers_output
|
||||
)
|
||||
if max_len != 0:
|
||||
base_element_len = max(len(elements[0]), len(elements[1]))
|
||||
temp_score = max_len / base_element_len
|
||||
score_list.append(temp_score)
|
||||
else:
|
||||
score_list.append(0)
|
||||
# base_element_len = max(len(elements[0]), len(elements[1]))
|
||||
# temp_score = max_len/base_element_len
|
||||
# score_list.append(temp_score)
|
||||
|
||||
score_list = sorted(score_list, reverse=True)
|
||||
return score_list
|
||||
|
||||
|
||||
def longest_common_prefix_with_lengths(list1, list2):
|
||||
"""
|
||||
计算两个二维列表中每个子列表的最长前缀匹配长度,并记录拥有最长前缀匹配长度的两个子列表的长度
|
||||
:param list1: 第一个二维列表
|
||||
:param list2: 第二个二维列表
|
||||
:return: 最长前缀匹配长度以及拥有最长前缀匹配长度的两个子列表的长度
|
||||
"""
|
||||
max_length = 0
|
||||
len_list1 = 0
|
||||
len_list2 = 0
|
||||
for i, sublist1 in enumerate(list1):
|
||||
for j, sublist2 in enumerate(list2):
|
||||
match_length = 0
|
||||
min_length = min(len(sublist1), len(sublist2))
|
||||
for k in range(min_length):
|
||||
if sublist1[k] == sublist2[k]:
|
||||
match_length += 1
|
||||
else:
|
||||
break
|
||||
if match_length > max_length:
|
||||
max_length = match_length
|
||||
len_list1 = len(sublist1)
|
||||
len_list2 = len(sublist2)
|
||||
return max_length, len_list1, len_list2
|
||||
|
||||
|
||||
def get_PM(answer_code: str, model_output_list: list, asnwer_name: str) -> list:
|
||||
"""
|
||||
计算PM,返回一个有序的得分列表
|
||||
:return:
|
||||
"""
|
||||
score_list = []
|
||||
for code in model_output_list:
|
||||
if '```python' in code:
|
||||
code = code.replace('```python', '')
|
||||
code = code.replace('```', '')
|
||||
if not re.search(rf'\b{re.escape(asnwer_name)}\b', code) or not is_code_valid(
|
||||
code
|
||||
):
|
||||
# if asnwer_name not in code or is_code_valid(code) == False:
|
||||
score_list.append(0)
|
||||
continue
|
||||
|
||||
# if asnwer_name not in code:
|
||||
# score_list.append(0)
|
||||
# continue
|
||||
|
||||
ans_list = get_token_per_line(answer_code)
|
||||
output_token_list = get_token_per_line(code)
|
||||
max_len, len1, len2 = longest_common_prefix_with_lengths(
|
||||
ans_list, output_token_list
|
||||
)
|
||||
base_element_len = max(len1, len2)
|
||||
|
||||
if base_element_len != 0:
|
||||
temp_score = max_len / base_element_len
|
||||
score_list.append(temp_score)
|
||||
else:
|
||||
score_list.append(0)
|
||||
|
||||
score_list = sorted(score_list, reverse=True)
|
||||
return score_list
|
||||
|
||||
|
||||
def get_score(score_list: list, k):
|
||||
"""
|
||||
计算score@n,k
|
||||
:param score_list:
|
||||
:param k:
|
||||
:return:
|
||||
"""
|
||||
n = len(score_list)
|
||||
sum = 0
|
||||
final = n - k + 1
|
||||
for i in range(1, final + 1):
|
||||
sum += math.comb(n - i, k - 1) * score_list[i - 1]
|
||||
|
||||
final_score = sum / math.comb(n, k)
|
||||
|
||||
return final_score
|
||||
|
||||
|
||||
k = 1
|
||||
task = 'block' # block or line
|
||||
json_name = f'Versicode_{task}_completion.json'
|
||||
|
||||
folder_path = f'../data/result_data/{task}_completion'
|
||||
model_list = os.listdir(folder_path)
|
||||
|
||||
for model in model_list:
|
||||
model_json_path = os.path.join(folder_path, model, json_name)
|
||||
with open(model_json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_dict = lodict
|
||||
data_list = data_dict
|
||||
data_len = len(data_list)
|
||||
sum_ISM = 0
|
||||
sum_PM = 0
|
||||
|
||||
for data in data_list:
|
||||
# model_output_list = eval(data['model_output'])
|
||||
model_output_list = eval(data['model_output_clear'])[:1]
|
||||
temp_list = []
|
||||
for o in model_output_list:
|
||||
temp_out = o.replace('```python', '')
|
||||
temp_out = temp_out.replace('```', '')
|
||||
temp_list.append(temp_out)
|
||||
model_output_list = temp_list
|
||||
answer_code = data['code']
|
||||
answer_name = data['core_token']
|
||||
#
|
||||
# answer_code = data['new_code'] #code editing
|
||||
# answer_name = data['new_name'] #code editing
|
||||
|
||||
# answer_code = data['old_code'] # code editing new to old
|
||||
# answer_name = data['old_name'] # code editing new to old
|
||||
#
|
||||
ISM_score_list = get_ISM(answer_code, model_output_list, answer_name)
|
||||
# ISM_score_without_verification_list = get_ISM_without_verification(answer_code, model_output_list, answer_name) #新增
|
||||
PM_score_list = get_PM(answer_code, model_output_list, answer_name)
|
||||
|
||||
# if not ISM_score_without_verification_list == ISM_score_list:#新增
|
||||
# for s in ISM_score_list:#新增
|
||||
# if s != ISM_score_without_verification_list[ISM_score_list.index(s)]:#新增
|
||||
# print('元数据如下')#新增
|
||||
# print(data)#新增
|
||||
# print('答案如下')#新增
|
||||
# print(model_output_list[ISM_score_list.index(s)])#新增
|
||||
|
||||
# flag = int(input('输入1继续,0退出'))#新增
|
||||
# if flag == 1:
|
||||
# continue
|
||||
|
||||
ISM_score = get_score(ISM_score_list, k)
|
||||
PM_score = get_score(PM_score_list, k)
|
||||
|
||||
sum_ISM += ISM_score
|
||||
sum_PM += PM_score
|
||||
# print(f"ISM分数:{ISM_score}")
|
||||
# print(f"PM分数:{PM_score}")
|
||||
|
||||
print(f'{model}, {task} completion task, ISM@{k} score: {sum_ISM / data_len}')
|
||||
print(f'{model}, {task} completion task, PM@{k} score: {sum_PM / data_len}')
|
||||
|
||||
|
||||
# def get_token(ans_code:str, output_code:str):
|
||||
# """
|
||||
# 对代码进行词法分析,分解成标识符,返回两个标识符列表
|
||||
# :param ans_code:
|
||||
# :param output_code:
|
||||
# :return:
|
||||
# """
|
||||
# tokens_ans = tokenize.tokenize(io.BytesIO(ans_code.encode('utf-8')).readline)
|
||||
# tokens_output = tokenize.tokenize(io.BytesIO(output_code.encode('utf-8')).readline)
|
||||
# identifiers_ans = []
|
||||
# identifiers_output = []
|
||||
# for token in tokens_ans:
|
||||
# if token.type == tokenize.NAME:
|
||||
# identifiers_ans.append(token.string)
|
||||
#
|
||||
# for to in tokens_output:
|
||||
# if to.type == tokenize.NAME:
|
||||
# identifiers_output.append(to.string)
|
||||
#
|
||||
# return identifiers_ans, identifiers_output
|
||||
@@ -1,198 +0,0 @@
|
||||
"""
|
||||
Calculate the cdc score for migration
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
|
||||
# warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
|
||||
def is_correct_parameter_count(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断参数数量是否一致
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 获取正确代码中的参数数量
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
expected_count = len(correct_param_list)
|
||||
else:
|
||||
expected_count = 0 # 如果没有参数,期望数量为0
|
||||
|
||||
# 在需要判断的代码中查找函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
return len(test_param_list) == expected_count # 检查参数数量
|
||||
else:
|
||||
# 如果没有括号,检查函数名是否在字符串中
|
||||
return expected_count == 0 and function_name in test_code
|
||||
|
||||
|
||||
def check_keyword_parameters(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断关键词参数赋值是否正确使用
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 正则表达式匹配正确代码中的函数调用
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
|
||||
# 检查待检测代码中的函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
|
||||
# 确保待检测的每个参数都以关键字参数形式赋值
|
||||
for correct_param in correct_param_list:
|
||||
if '=' in correct_param: # 仅当正确代码中有关键词参数
|
||||
param_name = correct_param.split('=')[0].strip()
|
||||
if not any(
|
||||
param_name in test_param and '=' in test_param
|
||||
for test_param in test_param_list
|
||||
):
|
||||
return False # 如果对应参数不是关键词参数,则返回False
|
||||
|
||||
return True # 所有关键字参数匹配
|
||||
|
||||
return False # 如果没有匹配,返回False
|
||||
|
||||
|
||||
def with_correct(answer_code: str, model_output: str) -> bool:
|
||||
"""
|
||||
当answer是with结构时,判断模型生成的是不是with结构
|
||||
:param answer_code:
|
||||
:param model_output:
|
||||
:return:
|
||||
"""
|
||||
# return True
|
||||
if not answer_code.startswith('with') and not model_output.startswith('with'):
|
||||
return True
|
||||
elif answer_code.startswith('with') and model_output.startswith('with'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def compute_block_score_k(
|
||||
answer: str,
|
||||
model_output: list,
|
||||
k: int,
|
||||
model_filled_code,
|
||||
core_line_in_core_block,
|
||||
core_line_in_output_clear,
|
||||
):
|
||||
"""
|
||||
cdc需要满足五个条件,em只需要满足第一个条件
|
||||
"""
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if (
|
||||
re.search(rf'\b{re.escape(answer)}\b', code)
|
||||
and is_code_valid(model_filled_code[index])
|
||||
and is_correct_parameter_count(
|
||||
answer, core_line_in_core_block, core_line_in_output_clear[index]
|
||||
)
|
||||
and with_correct(core_line_in_core_block, core_line_in_output_clear[index])
|
||||
and check_keyword_parameters(
|
||||
answer, core_line_in_core_block, core_line_in_output_clear[index]
|
||||
)
|
||||
): # block
|
||||
# if re.search(rf'\b{re.escape(answer)}\b', code):#block
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def is_code_valid(code):
|
||||
try:
|
||||
compile(code, '<string>', 'exec')
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def compute_score_k(answer: str, model_output: list, k: int):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for output in model_output:
|
||||
if '```python' in output:
|
||||
output = output.replace('```python', '')
|
||||
output = output.replace('```', '')
|
||||
# if answer == output:
|
||||
|
||||
if re.search(rf'\b{re.escape(answer)}\b', output) and is_code_valid(output):
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
k = 1 # cdc@k
|
||||
json_name = 'VersiCode_migration.json'
|
||||
task = 'migration'
|
||||
folder_path = '../data/result_data/code_migration'
|
||||
|
||||
model_list = os.listdir(folder_path)
|
||||
for model in model_list:
|
||||
# if model != 'gpt-4o':
|
||||
# continue
|
||||
model_json_path = os.path.join(folder_path, model, json_name)
|
||||
with open(model_json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_list = lodict
|
||||
|
||||
score_list = []
|
||||
for data in data_list:
|
||||
answer = data['new_name'] # old -> new
|
||||
model_output = data['model_output_clear'] # old -> new
|
||||
|
||||
model_filled_code = model_output
|
||||
# core_line_in_core_block = data['core_line_in_new_core_block']# old -> new
|
||||
core_line_in_core_block = data['core_line_in_code'] # old -> new
|
||||
core_line_in_output_clear = data['core_line_in_output_clear'] # old -> new
|
||||
|
||||
score_list.append(
|
||||
compute_block_score_k(
|
||||
answer,
|
||||
model_output,
|
||||
k,
|
||||
model_filled_code,
|
||||
core_line_in_core_block,
|
||||
core_line_in_output_clear,
|
||||
)
|
||||
)
|
||||
|
||||
final_score = sum(score_list) / len(score_list)
|
||||
print(f'{model}, {task} task, cdc@{k} score: {final_score}')
|
||||
@@ -1,225 +0,0 @@
|
||||
"""
|
||||
Calculate the cdc score for line and block
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
|
||||
# warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
|
||||
def is_code_valid(code):
|
||||
try:
|
||||
compile(code, '<string>', 'exec')
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_correct_parameter_count(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断参数数量是否一致
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 获取正确代码中的参数数量
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
expected_count = len(correct_param_list)
|
||||
else:
|
||||
expected_count = 0 # 如果没有参数,期望数量为0
|
||||
|
||||
# 在需要判断的代码中查找函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
return len(test_param_list) == expected_count # 检查参数数量
|
||||
else:
|
||||
# 如果没有括号,检查函数名是否在字符串中
|
||||
return expected_count == 0 and function_name in test_code
|
||||
|
||||
|
||||
def check_keyword_parameters(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断关键词参数赋值是否正确使用
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 正则表达式匹配正确代码中的函数调用
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
|
||||
# 检查待检测代码中的函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
|
||||
# 确保待检测的每个参数都以关键字参数形式赋值
|
||||
for correct_param in correct_param_list:
|
||||
if '=' in correct_param: # 仅当正确代码中有关键词参数
|
||||
param_name = correct_param.split('=')[0].strip()
|
||||
if not any(
|
||||
param_name in test_param and '=' in test_param
|
||||
for test_param in test_param_list
|
||||
):
|
||||
return False # 如果对应参数不是关键词参数,则返回False
|
||||
|
||||
return True # 所有关键字参数匹配
|
||||
|
||||
return False # 如果没有匹配,返回False
|
||||
|
||||
|
||||
def with_correct(answer_code: str, model_output: str) -> bool:
|
||||
"""
|
||||
当answer是with结构时,判断模型生成的是不是with结构
|
||||
:param answer_code:
|
||||
:param model_output:
|
||||
:return:
|
||||
"""
|
||||
# return True
|
||||
if not answer_code.startswith('with') and not model_output.startswith('with'):
|
||||
return True
|
||||
elif answer_code.startswith('with') and model_output.startswith('with'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def compute_line_score_k(
|
||||
answer: str, model_output: list, k: int, model_filled_code, core_line
|
||||
):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if (
|
||||
re.search(rf'\b{re.escape(answer)}\b', code)
|
||||
and is_code_valid(model_filled_code[index])
|
||||
and is_correct_parameter_count(answer, core_line, code)
|
||||
and with_correct(core_line, code)
|
||||
and check_keyword_parameters(answer, core_line, code)
|
||||
): # line
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def compute_block_score_k(
|
||||
answer: str,
|
||||
model_output: list,
|
||||
k: int,
|
||||
model_filled_code,
|
||||
core_line_in_core_block,
|
||||
core_line_in_output_clear,
|
||||
):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if (
|
||||
re.search(rf'\b{re.escape(answer)}\b', code)
|
||||
and is_code_valid(model_filled_code[index])
|
||||
and is_correct_parameter_count(
|
||||
answer, core_line_in_core_block, core_line_in_output_clear[index]
|
||||
)
|
||||
and with_correct(core_line_in_core_block, core_line_in_output_clear[index])
|
||||
and check_keyword_parameters(
|
||||
answer, core_line_in_core_block, core_line_in_output_clear[index]
|
||||
)
|
||||
): # block
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def compute_score_k(answer: str, model_output: list, k: int):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if re.search(rf'\b{re.escape(answer)}\b', code) and is_code_valid(
|
||||
code
|
||||
): # block
|
||||
# if re.search(rf'\b{re.escape(answer)}\b', code):#line
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
k = 3 # cdc@k
|
||||
task = 'block' # line or block
|
||||
json_name = f'Versicode_{task}_completion.json'
|
||||
|
||||
folder_path = f'../data/result_data/{task}_completion'
|
||||
model_list = os.listdir(folder_path)
|
||||
|
||||
for model in model_list:
|
||||
model_json_path = os.path.join(folder_path, model, json_name)
|
||||
with open(model_json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_list = lodict
|
||||
|
||||
if task == 'line':
|
||||
score_list = []
|
||||
for data in data_list:
|
||||
answer = data['core_token']
|
||||
model_output = eval(data['model_output_clear'])
|
||||
model_filled_code = [
|
||||
data['masked_code'].replace('<mask>', i) for i in model_output
|
||||
]
|
||||
core_line = data['core_line']
|
||||
score_list.append(
|
||||
compute_line_score_k(
|
||||
answer, model_output, k, model_filled_code, core_line
|
||||
)
|
||||
)
|
||||
else:
|
||||
score_list = []
|
||||
for data in data_list:
|
||||
answer = data['core_token']
|
||||
model_output = eval(data['model_output_clear'])
|
||||
model_filled_code = eval(data['model_output_clear'])
|
||||
core_line = data['core_line']
|
||||
core_line_in_output_clear = data['core_line_in_output_clear']
|
||||
score_list.append(
|
||||
compute_block_score_k(
|
||||
answer,
|
||||
model_output,
|
||||
k,
|
||||
model_filled_code,
|
||||
core_line,
|
||||
core_line_in_output_clear,
|
||||
)
|
||||
)
|
||||
|
||||
final_score = sum(score_list) / len(score_list)
|
||||
print(f'{model}, {task} completion task, cdc@{k} score: {final_score}')
|
||||
@@ -1,209 +0,0 @@
|
||||
"""
|
||||
Calculate the cdc score for line and block
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
|
||||
# warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
|
||||
def is_code_valid(code):
|
||||
try:
|
||||
compile(code, '<string>', 'exec')
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_correct_parameter_count(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断参数数量是否一致
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 获取正确代码中的参数数量
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
expected_count = len(correct_param_list)
|
||||
else:
|
||||
expected_count = 0 # 如果没有参数,期望数量为0
|
||||
|
||||
# 在需要判断的代码中查找函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
return len(test_param_list) == expected_count # 检查参数数量
|
||||
else:
|
||||
# 如果没有括号,检查函数名是否在字符串中
|
||||
return expected_count == 0 and function_name in test_code
|
||||
|
||||
|
||||
def check_keyword_parameters(function_name, correct_code, test_code):
|
||||
"""
|
||||
判断关键词参数赋值是否正确使用
|
||||
:param function_name:
|
||||
:param correct_code:
|
||||
:param test_code:
|
||||
:return:
|
||||
"""
|
||||
# 正则表达式匹配正确代码中的函数调用
|
||||
# return True
|
||||
pattern = rf'{function_name}\((.*?)\)'
|
||||
correct_match = re.search(pattern, correct_code)
|
||||
|
||||
if correct_match:
|
||||
correct_params = correct_match.group(1).strip()
|
||||
correct_param_list = [p.strip() for p in correct_params.split(',') if p.strip()]
|
||||
|
||||
# 检查待检测代码中的函数调用
|
||||
test_match = re.search(pattern, test_code)
|
||||
|
||||
if test_match:
|
||||
test_params = test_match.group(1).strip()
|
||||
test_param_list = [p.strip() for p in test_params.split(',') if p.strip()]
|
||||
|
||||
# 确保待检测的每个参数都以关键字参数形式赋值
|
||||
for correct_param in correct_param_list:
|
||||
if '=' in correct_param: # 仅当正确代码中有关键词参数
|
||||
param_name = correct_param.split('=')[0].strip()
|
||||
if not any(
|
||||
param_name in test_param and '=' in test_param
|
||||
for test_param in test_param_list
|
||||
):
|
||||
return False # 如果对应参数不是关键词参数,则返回False
|
||||
|
||||
return True # 所有关键字参数匹配
|
||||
|
||||
return False # 如果没有匹配,返回False
|
||||
|
||||
|
||||
def with_correct(answer_code: str, model_output: str) -> bool:
|
||||
"""
|
||||
当answer是with结构时,判断模型生成的是不是with结构
|
||||
:param answer_code:
|
||||
:param model_output:
|
||||
:return:
|
||||
"""
|
||||
# return True
|
||||
if not answer_code.startswith('with') and not model_output.startswith('with'):
|
||||
return True
|
||||
elif answer_code.startswith('with') and model_output.startswith('with'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def compute_line_score_k(
|
||||
answer: str, model_output: list, k: int, model_filled_code, core_line
|
||||
):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if re.search(rf'\b{re.escape(answer)}\b', code): # line
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def compute_block_score_k(
|
||||
answer: str,
|
||||
model_output: list,
|
||||
k: int,
|
||||
model_filled_code,
|
||||
core_line_in_core_block,
|
||||
core_line_in_output_clear,
|
||||
):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if re.search(rf'\b{re.escape(answer)}\b', code): # block
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def compute_score_k(answer: str, model_output: list, k: int):
|
||||
c = 0
|
||||
n = len(model_output)
|
||||
for index, code in enumerate(model_output):
|
||||
if re.search(rf'\b{re.escape(answer)}\b', code) and is_code_valid(
|
||||
code
|
||||
): # block
|
||||
# if re.search(rf'\b{re.escape(answer)}\b', code):#line
|
||||
c += 1
|
||||
if n - c < k:
|
||||
return 1.0
|
||||
|
||||
score = 1 - (math.comb(n - c, k)) / (math.comb(n, k))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
k = 3 # em@k
|
||||
task = 'block' # line or block
|
||||
json_name = f'Versicode_{task}_completion.json'
|
||||
|
||||
folder_path = f'../data/result_data/{task}_completion'
|
||||
model_list = os.listdir(folder_path)
|
||||
|
||||
for model in model_list:
|
||||
model_json_path = os.path.join(folder_path, model, json_name)
|
||||
with open(model_json_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_list = lodict
|
||||
|
||||
if task == 'line':
|
||||
score_list = []
|
||||
for data in data_list:
|
||||
answer = data['core_token']
|
||||
model_output = eval(data['model_output_clear'])
|
||||
model_filled_code = [
|
||||
data['masked_code'].replace('<mask>', i) for i in model_output
|
||||
]
|
||||
core_line = data['core_line']
|
||||
score_list.append(
|
||||
compute_line_score_k(
|
||||
answer, model_output, k, model_filled_code, core_line
|
||||
)
|
||||
)
|
||||
else:
|
||||
score_list = []
|
||||
for data in data_list:
|
||||
answer = data['core_token']
|
||||
model_output = eval(data['model_output_clear'])
|
||||
model_filled_code = eval(data['model_output_clear'])
|
||||
core_line = data['core_line']
|
||||
core_line_in_output_clear = data['core_line_in_output_clear']
|
||||
score_list.append(
|
||||
compute_block_score_k(
|
||||
answer,
|
||||
model_output,
|
||||
k,
|
||||
model_filled_code,
|
||||
core_line,
|
||||
core_line_in_output_clear,
|
||||
)
|
||||
)
|
||||
|
||||
final_score = sum(score_list) / len(score_list)
|
||||
print(f'{model}, {task} completion task, em@{k} score: {final_score}')
|
||||
@@ -1,99 +0,0 @@
|
||||
"""
|
||||
Find the line of code generated by the model using the block in the version code
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
|
||||
def process_line_mask(code_snippet, core_token):
|
||||
if not core_token:
|
||||
return None, None
|
||||
|
||||
replaced_lines = {}
|
||||
lines = code_snippet.split('\n')
|
||||
|
||||
in_multi_line_comment = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if in_multi_line_comment:
|
||||
if ('"""' in line or "'''" in line) and not re.findall(
|
||||
r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line
|
||||
):
|
||||
in_multi_line_comment = False
|
||||
continue
|
||||
elif line.strip().startswith('#'):
|
||||
continue
|
||||
elif re.findall(r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line):
|
||||
continue
|
||||
elif ('"""' in line or "'''" in line) and not re.findall(
|
||||
r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line
|
||||
):
|
||||
in_multi_line_comment = True
|
||||
continue
|
||||
else:
|
||||
if re.search(r'\bdef\s+task_function\b', line):
|
||||
continue
|
||||
|
||||
if re.search(r'\b{}\b(?!\s*=)'.format(re.escape(core_token)), line):
|
||||
replaced_lines.update({i: line})
|
||||
|
||||
if replaced_lines:
|
||||
random_line_location = random.choice(list(replaced_lines.keys()))
|
||||
|
||||
masked_line = lines[random_line_location]
|
||||
leading_spaces = re.match(r'^\s*', masked_line).group(0)
|
||||
masked_line = masked_line.strip()
|
||||
lines[random_line_location] = leading_spaces + '<line_mask>'
|
||||
|
||||
masked_code = '\n'.join(lines)
|
||||
|
||||
return masked_code, masked_line
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def load_json(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_json(file_path, data):
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
model_list = os.listdir('../data/result_data/block_completion')
|
||||
for model in model_list:
|
||||
input_json_file = f'../data/result_data/block_completion/{model}/VersiCode_block_completion.json'
|
||||
output_json_file = input_json_file
|
||||
data = load_json(input_json_file)
|
||||
|
||||
for item in data:
|
||||
core_token = item['core_token']
|
||||
code = item['code']
|
||||
|
||||
_, core_line_in_code = process_line_mask(code, core_token)
|
||||
if core_line_in_code:
|
||||
item['core_line_in_code'] = core_line_in_code
|
||||
else:
|
||||
item['core_line_in_code'] = 'N/A'
|
||||
|
||||
model_output_clear = item['model_output_clear']
|
||||
core_line_in_output_list = []
|
||||
|
||||
for entry in eval(model_output_clear):
|
||||
_, core_line_in_output = process_line_mask(entry, core_token)
|
||||
if core_line_in_output:
|
||||
core_line_in_output_list.append(core_line_in_output)
|
||||
else:
|
||||
core_line_in_output_list.append('N/A')
|
||||
|
||||
item['core_line_in_output_clear'] = core_line_in_output_list
|
||||
|
||||
save_json(output_json_file, data)
|
||||
print('Done!')
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Find the line of code generated by the model using the block in the version code
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
|
||||
def process_line_mask(code_snippet, core_token):
|
||||
if not core_token:
|
||||
return None, None
|
||||
|
||||
replaced_lines = {}
|
||||
lines = code_snippet.split('\n')
|
||||
|
||||
in_multi_line_comment = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if in_multi_line_comment:
|
||||
if ('"""' in line or "'''" in line) and not re.findall(
|
||||
r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line
|
||||
):
|
||||
in_multi_line_comment = False
|
||||
continue
|
||||
elif line.strip().startswith('#'):
|
||||
continue
|
||||
elif re.findall(r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line):
|
||||
continue
|
||||
elif ('"""' in line or "'''" in line) and not re.findall(
|
||||
r"'''(.*?)'''|\"\"\"(.*?)\"\"\"", line
|
||||
):
|
||||
in_multi_line_comment = True
|
||||
continue
|
||||
else:
|
||||
if re.search(r'\bdef\s+task_function\b', line):
|
||||
continue
|
||||
|
||||
if re.search(r'\b{}\b(?!\s*=)'.format(re.escape(core_token)), line):
|
||||
replaced_lines.update({i: line})
|
||||
|
||||
if replaced_lines:
|
||||
random_line_location = random.choice(list(replaced_lines.keys()))
|
||||
|
||||
masked_line = lines[random_line_location]
|
||||
leading_spaces = re.match(r'^\s*', masked_line).group(0)
|
||||
masked_line = masked_line.strip()
|
||||
lines[random_line_location] = leading_spaces + '<line_mask>'
|
||||
|
||||
masked_code = '\n'.join(lines)
|
||||
|
||||
return masked_code, masked_line
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def load_json(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_json(file_path, data):
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
model_list = os.listdir('../data/result_data/code_migration')
|
||||
for model in model_list:
|
||||
input_json_file = (
|
||||
f'../data/result_data/code_migration/{model}/VersiCode_migration.json'
|
||||
)
|
||||
output_json_file = input_json_file
|
||||
data = load_json(input_json_file)
|
||||
|
||||
for item in data:
|
||||
core_token = item['old_name']
|
||||
code = item['old_code']
|
||||
|
||||
_, core_line_in_code = process_line_mask(code, core_token)
|
||||
if core_line_in_code:
|
||||
item['core_line_in_code'] = core_line_in_code
|
||||
else:
|
||||
item['core_line_in_code'] = 'N/A'
|
||||
|
||||
model_output_clear = item['model_output_clear']
|
||||
core_line_in_output_list = []
|
||||
|
||||
core_token = item['new_name']
|
||||
for entry in eval(model_output_clear):
|
||||
_, core_line_in_output = process_line_mask(entry, core_token)
|
||||
if core_line_in_output:
|
||||
core_line_in_output_list.append(core_line_in_output)
|
||||
else:
|
||||
core_line_in_output_list.append('N/A')
|
||||
|
||||
item['core_line_in_output_clear'] = core_line_in_output_list
|
||||
|
||||
save_json(output_json_file, data)
|
||||
print('Done!')
|
||||
@@ -1,38 +0,0 @@
|
||||
"""
|
||||
Clear the<start>and<end>generated by the model in inference
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
model_name = ''
|
||||
task = 'block_completion'
|
||||
|
||||
result_path = f'../data/result_data/{task}/{model_name}/VersiCode_block_completion.json' # Modify the file according to the task format
|
||||
|
||||
|
||||
with open(result_path, 'r', encoding='utf-8') as fr:
|
||||
lodict = json.load(fr)
|
||||
data_dict = lodict
|
||||
data_list = data_dict
|
||||
|
||||
for data in data_list:
|
||||
temp_list = []
|
||||
model_output_list = eval(data['model_output'])
|
||||
for output in model_output_list:
|
||||
if '<start>' in output and '<end>' in output:
|
||||
start_index = output.find('<start>') + len('<start>')
|
||||
end_index = output.find('<end>')
|
||||
content = (
|
||||
output[start_index:end_index]
|
||||
.replace('```python', '')
|
||||
.replace('```', '')
|
||||
)
|
||||
else:
|
||||
content = 'no_answer'
|
||||
|
||||
temp_list.append(content)
|
||||
|
||||
data['model_output_clear'] = str(temp_list)
|
||||
|
||||
with open(result_path, 'w', encoding='utf-8') as fw:
|
||||
json.dump(data_dict, fw, indent=4, ensure_ascii=False)
|
||||
@@ -1,146 +0,0 @@
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.11.18
|
||||
aiosignal==1.3.2
|
||||
airportsdata==20250224
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
astor==0.8.1
|
||||
attrs==25.3.0
|
||||
blake3==1.0.4
|
||||
cachetools==5.5.2
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
cloudpickle==3.1.1
|
||||
compressed-tensors==0.9.3
|
||||
cupy-cuda12x==13.4.1
|
||||
Deprecated==1.2.18
|
||||
depyf==0.18.0
|
||||
dill==0.4.0
|
||||
diskcache==5.6.3
|
||||
distro==1.9.0
|
||||
dnspython==2.7.0
|
||||
einops==0.8.1
|
||||
email_validator==2.2.0
|
||||
fastapi==0.115.12
|
||||
fastapi-cli==0.0.7
|
||||
fastrlock==0.8.3
|
||||
filelock==3.18.0
|
||||
frozenlist==1.6.0
|
||||
fsspec==2025.3.2
|
||||
gguf==0.16.2
|
||||
googleapis-common-protos==1.70.0
|
||||
grpcio==1.71.0
|
||||
h11==0.14.0
|
||||
hf-xet==1.0.3
|
||||
httpcore==1.0.8
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
huggingface-hub==0.30.2
|
||||
idna==3.10
|
||||
importlib_metadata==8.0.0
|
||||
interegular==0.3.3
|
||||
Jinja2==3.1.6
|
||||
jiter==0.9.0
|
||||
jsonschema==4.23.0
|
||||
jsonschema-specifications==2024.10.1
|
||||
lark==1.2.2
|
||||
llguidance==0.7.16
|
||||
llvmlite==0.44.0
|
||||
lm-format-enforcer==0.10.11
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
mdurl==0.1.2
|
||||
mistral_common==1.5.4
|
||||
mpmath==1.3.0
|
||||
msgpack==1.1.0
|
||||
msgspec==0.19.0
|
||||
multidict==6.4.3
|
||||
nest-asyncio==1.6.0
|
||||
networkx==3.4.2
|
||||
ninja==1.11.1.4
|
||||
numba==0.61.2
|
||||
numpy==2.2.5
|
||||
nvidia-cublas-cu12==12.4.5.8
|
||||
nvidia-cuda-cupti-cu12==12.4.127
|
||||
nvidia-cuda-nvrtc-cu12==12.4.127
|
||||
nvidia-cuda-runtime-cu12==12.4.127
|
||||
nvidia-cudnn-cu12==9.1.0.70
|
||||
nvidia-cufft-cu12==11.2.1.3
|
||||
nvidia-curand-cu12==10.3.5.147
|
||||
nvidia-cusolver-cu12==11.6.1.9
|
||||
nvidia-cusparse-cu12==12.3.1.170
|
||||
nvidia-cusparselt-cu12==0.6.2
|
||||
nvidia-nccl-cu12==2.21.5
|
||||
nvidia-nvjitlink-cu12==12.4.127
|
||||
nvidia-nvtx-cu12==12.4.127
|
||||
openai==1.75.0
|
||||
opencv-python-headless==4.11.0.86
|
||||
opentelemetry-api==1.26.0
|
||||
opentelemetry-exporter-otlp==1.26.0
|
||||
opentelemetry-exporter-otlp-proto-common==1.26.0
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.26.0
|
||||
opentelemetry-exporter-otlp-proto-http==1.26.0
|
||||
opentelemetry-proto==1.26.0
|
||||
opentelemetry-sdk==1.26.0
|
||||
opentelemetry-semantic-conventions==0.47b0
|
||||
opentelemetry-semantic-conventions-ai==0.4.3
|
||||
outlines==0.1.11
|
||||
outlines_core==0.1.26
|
||||
packaging==25.0
|
||||
partial-json-parser==0.2.1.1.post5
|
||||
pillow==11.2.1
|
||||
prometheus-fastapi-instrumentator==7.1.0
|
||||
prometheus_client==0.21.1
|
||||
propcache==0.3.1
|
||||
protobuf==4.25.6
|
||||
psutil==7.0.0
|
||||
py-cpuinfo==9.0.0
|
||||
pycountry==24.6.1
|
||||
pydantic==2.11.3
|
||||
pydantic_core==2.33.1
|
||||
Pygments==2.19.1
|
||||
python-dotenv==1.1.0
|
||||
python-json-logger==3.3.0
|
||||
python-multipart==0.0.20
|
||||
PyYAML==6.0.2
|
||||
pyzmq==26.4.0
|
||||
ray==2.43.0
|
||||
referencing==0.36.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.3
|
||||
rich==14.0.0
|
||||
rich-toolkit==0.14.1
|
||||
rpds-py==0.24.0
|
||||
safetensors==0.5.3
|
||||
scipy==1.15.2
|
||||
sentencepiece==0.2.0
|
||||
setuptools==75.8.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
starlette==0.46.2
|
||||
sympy==1.13.1
|
||||
tiktoken==0.9.0
|
||||
tokenizers==0.21.1
|
||||
torch==2.6.0
|
||||
torchaudio==2.6.0
|
||||
torchvision==0.21.0
|
||||
tqdm==4.67.1
|
||||
transformers==4.51.3
|
||||
triton==3.2.0
|
||||
typer==0.15.2
|
||||
typing-inspection==0.4.0
|
||||
typing_extensions==4.13.2
|
||||
urllib3==2.4.0
|
||||
uvicorn==0.34.2
|
||||
uvloop==0.21.0
|
||||
vllm==0.8.4
|
||||
watchfiles==1.0.5
|
||||
websockets==15.0.1
|
||||
wheel==0.45.1
|
||||
wrapt==1.17.2
|
||||
xformers==0.0.29.post2
|
||||
xgrammar==0.1.18
|
||||
yarl==1.20.0
|
||||
zipp==3.21.0
|
||||
@@ -16,8 +16,8 @@ vi.mock("react-i18next", async () => {
|
||||
if (i18nKey === "SETTINGS$API_KEYS_DESCRIPTION") {
|
||||
return (
|
||||
<span>
|
||||
API keys allow you to authenticate with the OpenHands API programmatically.
|
||||
Keep your API keys secure; anyone with your API key can access your account.
|
||||
API keys allow you to authenticate with the OpenHands API programmatically.
|
||||
Keep your API keys secure; anyone with your API key can access your account.
|
||||
For more information on how to use the API, see our {components.a}
|
||||
</span>
|
||||
);
|
||||
@@ -48,7 +48,7 @@ describe("ApiKeysManager", () => {
|
||||
|
||||
it("should render the API documentation link", () => {
|
||||
renderComponent();
|
||||
|
||||
|
||||
// Find the link to the API documentation
|
||||
const link = screen.getByRole("link");
|
||||
expect(link).toBeInTheDocument();
|
||||
@@ -56,4 +56,4 @@ describe("ApiKeysManager", () => {
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -89,9 +89,6 @@ describe("Content", () => {
|
||||
await screen.findByTestId("gitlab-token-input");
|
||||
await screen.findByTestId("gitlab-token-help-anchor");
|
||||
|
||||
await screen.findByTestId("azure-devops-token-input");
|
||||
await screen.findByTestId("azure-devops-token-help-anchor");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_SAAS_CONFIG);
|
||||
queryClient.invalidateQueries();
|
||||
rerender();
|
||||
@@ -110,13 +107,6 @@ describe("Content", () => {
|
||||
expect(
|
||||
screen.queryByTestId("gitlab-token-help-anchor"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("azure-devops-token-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("azure-devops-token-help-anchor"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,12 +133,6 @@ describe("Content", () => {
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const azureDevOpsInput = screen.getByTestId("azure-devops-token-input");
|
||||
expect(azureDevOpsInput).toHaveProperty("placeholder", "");
|
||||
expect(
|
||||
screen.queryByTestId("ado-set-token-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -156,7 +140,6 @@ describe("Content", () => {
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: null,
|
||||
azure_devops: null,
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
@@ -175,19 +158,12 @@ describe("Content", () => {
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const azureDevOpsInput = screen.getByTestId("azure-devops-token-input");
|
||||
expect(azureDevOpsInput).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(
|
||||
screen.queryByTestId("ado-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
gitlab: null,
|
||||
azure_devops: null,
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
@@ -206,12 +182,6 @@ describe("Content", () => {
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const azureDevOpsInput = screen.getByTestId("azure-devops-token-input");
|
||||
expect(azureDevOpsInput).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(
|
||||
screen.queryByTestId("ado-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,49 +243,15 @@ describe("Form submission", () => {
|
||||
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
||||
github: { token: "test-token", host: "" },
|
||||
gitlab: { token: "", host: "" },
|
||||
azure_devops: { token: "", host: "" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should save the GitLab token", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
|
||||
await userEvent.type(gitlabInput, "test-token");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
||||
github: { token: "", host: "" },
|
||||
gitlab: { token: "test-token", host: "" },
|
||||
azure_devops: { token: "", host: "" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should save the Azure DevOps token", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const azureDevOpsInput = await screen.findByTestId("azure-devops-token-input");
|
||||
const azureDevOpsHostInput = await screen.findByTestId("azure-devops-host-input");
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
|
||||
await userEvent.type(azureDevOpsInput, "test-token");
|
||||
await userEvent.type(azureDevOpsHostInput, "https://dev.azure.com/test-org");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
||||
github: { token: "", host: "" },
|
||||
github: { token: "test-token", host: "" },
|
||||
gitlab: { token: "", host: "" },
|
||||
azure_devops: { token: "test-token", host: "https://dev.azure.com/test-org" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -343,14 +279,6 @@ describe("Form submission", () => {
|
||||
|
||||
await userEvent.clear(gitlabInput);
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const azureDevOpsInput = await screen.findByTestId("azure-devops-token-input");
|
||||
await userEvent.type(azureDevOpsInput, "test-token");
|
||||
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(azureDevOpsInput);
|
||||
expect(submit).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable a disconnect tokens button if there is at least one token set", async () => {
|
||||
@@ -363,7 +291,6 @@ describe("Form submission", () => {
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: null,
|
||||
azure_devops: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -395,7 +322,6 @@ describe("Form submission", () => {
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: null,
|
||||
azure_devops: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -334,7 +334,10 @@ describe("Settings 404", () => {
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,4 +39,4 @@ describe("Check for hardcoded English strings in Home components", () => {
|
||||
expect(text).not.toContain(str);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1308
frontend/package-lock.json
generated
1308
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.43.0",
|
||||
"version": "0.42.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -10,30 +10,30 @@
|
||||
"@heroui/react": "^2.8.0-beta.7",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@react-router/node": "^7.6.1",
|
||||
"@react-router/serve": "^7.6.1",
|
||||
"@react-types/shared": "^3.29.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"@stripe/stripe-js": "^7.3.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.77.2",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.17.3",
|
||||
"framer-motion": "^12.16.0",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.28",
|
||||
"jose": "^6.0.11",
|
||||
"lucide-react": "^0.514.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.251.0",
|
||||
"posthog-js": "^1.245.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -42,15 +42,15 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.6.2",
|
||||
"react-router": "^7.6.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"vite": "^6.3.5",
|
||||
"web-vitals": "^5.0.3",
|
||||
"web-vitals": "^5.0.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -84,23 +84,23 @@
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.53.0",
|
||||
"@react-router/dev": "^7.6.2",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@react-router/dev": "^7.6.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"@vitest/coverage-v8": "^3.1.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -115,10 +115,10 @@
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^18.2.1",
|
||||
"stripe": "^18.1.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
|
||||
@@ -5,23 +5,24 @@
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.10.2'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const PACKAGE_VERSION = '2.8.4'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener('install', function () {
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener('activate', function (event) {
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
@@ -93,18 +94,17 @@ addEventListener('message', async function (event) {
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === 'navigate') {
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,62 +115,48 @@ addEventListener('fetch', function (event) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {string} requestId
|
||||
*/
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
;(async function () {
|
||||
const responseClone = response.clone()
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
[responseClone.body],
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main client for the given event.
|
||||
* Client that issues a request doesn't necessarily equal the client
|
||||
* that registered the worker. It's with the latter the worker should
|
||||
* communicate with during the response resolving phase.
|
||||
* @param {FetchEvent} event
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
@@ -198,16 +184,12 @@ async function resolveMainClient(event) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone()
|
||||
const requestClone = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
@@ -248,17 +230,29 @@ async function getResponse(event, client, requestId) {
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const requestBuffer = await request.arrayBuffer()
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: requestBuffer,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
[requestBuffer],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
@@ -274,12 +268,6 @@ async function getResponse(event, client, requestId) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {any} message
|
||||
* @param {Array<Transferable>} transferrables
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
@@ -292,18 +280,14 @@ function sendToClient(client, message, transferrables = []) {
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
client.postMessage(
|
||||
message,
|
||||
[channel.port2].concat(transferrables.filter(Boolean)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns {Response}
|
||||
*/
|
||||
function respondWithMock(response) {
|
||||
async function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
@@ -321,24 +305,3 @@ function respondWithMock(response) {
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
*/
|
||||
async function serializeRequest(request) {
|
||||
return {
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ Object.entries(translationJson).forEach(([key, translations]) => {
|
||||
if (Object.keys(missingTranslations).length > 0) {
|
||||
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Missing translations detected');
|
||||
console.error(`Found ${Object.keys(missingTranslations).length} translation keys with missing languages:`);
|
||||
|
||||
|
||||
Object.entries(missingTranslations).forEach(([key, langs]) => {
|
||||
console.error(`- Key "${key}" is missing translations for: ${langs.join(', ')}`);
|
||||
});
|
||||
|
||||
|
||||
console.error('\nPlease add the missing translations before committing.');
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ if (Object.keys(missingTranslations).length > 0) {
|
||||
if (Object.keys(extraLanguages).length > 0) {
|
||||
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Extra languages detected');
|
||||
console.error(`Found ${Object.keys(extraLanguages).length} translation keys with extra languages not in AvailableLanguages:`);
|
||||
|
||||
|
||||
Object.entries(extraLanguages).forEach(([key, langs]) => {
|
||||
console.error(`- Key "${key}" has translations for unsupported languages: ${langs.join(', ')}`);
|
||||
});
|
||||
|
||||
|
||||
console.error('\nPlease remove the extra languages before committing.');
|
||||
}
|
||||
|
||||
@@ -85,4 +85,4 @@ if (hasErrors) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\x1b[32m%s\x1b[0m', 'All translation keys have complete language coverage!');
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 18-5 4-8-3v3l-4.19-5.75 12.91 1.05v-10.96l4.28-.69zm-17.19-1.75v-7.29l12.91-2.62-7.12-4.34v2.84l-6.63 1.92-1.97 2.62v5.69z"/></svg>
|
||||
|
Before Width: | Height: | Size: 228 B |
@@ -20,31 +20,19 @@ export function ActionSuggestions({
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isGitLab = providers.includes("gitlab");
|
||||
const isAzureDevOps = providers.includes("azure_devops");
|
||||
|
||||
// Determine the correct terminology based on the provider
|
||||
let pr;
|
||||
let prShort;
|
||||
let providerName;
|
||||
if (isGitLab) {
|
||||
pr = "merge request";
|
||||
prShort = "MR";
|
||||
providerName = "GitLab";
|
||||
} else if (isAzureDevOps) {
|
||||
pr = "pull request";
|
||||
prShort = "PR";
|
||||
providerName = "Azure DevOps";
|
||||
} else {
|
||||
pr = "pull request";
|
||||
prShort = "PR";
|
||||
providerName = "GitHub";
|
||||
}
|
||||
const pr = isGitLab ? "merge request" : "pull request";
|
||||
const prShort = isGitLab ? "MR" : "PR";
|
||||
|
||||
const terms = {
|
||||
pr,
|
||||
prShort,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${providerName}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
|
||||
createPR: `Please push the changes to ${providerName} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${
|
||||
isGitLab ? "GitLab" : "GitHub"
|
||||
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
|
||||
createPR: `Please push the changes to ${
|
||||
isGitLab ? "GitLab" : "GitHub"
|
||||
} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
|
||||
pushToPR: `Please push the latest changes to the existing ${pr}.`,
|
||||
};
|
||||
|
||||
|
||||
@@ -54,17 +54,7 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
const issueType =
|
||||
task.task_type === "OPEN_ISSUE" ? "issues" : "merge_requests";
|
||||
href = `https://gitlab.com/${task.repo}/-/${issueType}/${task.issue_number}`;
|
||||
} else if (task.git_provider === "azure_devops") {
|
||||
// Azure DevOps URLs format: https://dev.azure.com/{organization}/{project}/_workitems/edit/{id}
|
||||
// For pull requests: https://dev.azure.com/{organization}/{project}/_git/{repository}/pullrequest/{id}
|
||||
const [project, repository] = task.repo.split("/");
|
||||
if (task.task_type === "OPEN_ISSUE") {
|
||||
href = `https://dev.azure.com/${project}/_workitems/edit/${task.issue_number}`;
|
||||
} else {
|
||||
href = `https://dev.azure.com/${project}/_git/${repository}/pullrequest/${task.issue_number}`;
|
||||
}
|
||||
} else {
|
||||
// Default to GitHub
|
||||
const hrefType = task.task_type === "OPEN_ISSUE" ? "issues" : "pull";
|
||||
href = `https://github.com/${task.repo}/${hrefType}/${task.issue_number}`;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function AzureDevOpsTokenHelpAnchor() {
|
||||
return (
|
||||
<p data-testid="azure-devops-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
i18nKey={I18nKey.AZURE_DEVOPS$TOKEN_HELP_TEXT}
|
||||
components={[
|
||||
<a
|
||||
key="azure-devops-token-help-anchor-link"
|
||||
aria-label="Azure DevOps token help link"
|
||||
href="https://dev.azure.com/_usersSettings/tokens"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
<a
|
||||
key="azure-devops-token-help-anchor-link-2"
|
||||
aria-label="Azure DevOps token see more link"
|
||||
href="https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
import { AzureDevOpsTokenHelpAnchor } from "./azure-devops-token-help-anchor";
|
||||
import { KeyStatusIcon } from "../key-status-icon";
|
||||
|
||||
interface AzureDevOpsTokenInputProps {
|
||||
onChange: (value: string) => void;
|
||||
onAzureDevOpsHostChange: (value: string) => void;
|
||||
isAzureDevOpsTokenSet: boolean;
|
||||
name: string;
|
||||
azureDevOpsHostSet: string | null | undefined;
|
||||
}
|
||||
|
||||
export function AzureDevOpsTokenInput({
|
||||
onChange,
|
||||
onAzureDevOpsHostChange,
|
||||
isAzureDevOpsTokenSet,
|
||||
name,
|
||||
azureDevOpsHostSet,
|
||||
}: AzureDevOpsTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.AZURE_DEVOPS$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isAzureDevOpsTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isAzureDevOpsTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="ado-set-token-indicator"
|
||||
isSet={isAzureDevOpsTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
onChange={onAzureDevOpsHostChange || (() => {})}
|
||||
name="azure-devops-host-input"
|
||||
testId="azure-devops-host-input"
|
||||
label={t(I18nKey.AZURE_DEVOPS$HOST_LABEL)}
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
placeholder="https://dev.azure.com/{your-org-name}"
|
||||
defaultValue={azureDevOpsHostSet || undefined}
|
||||
startContent={
|
||||
azureDevOpsHostSet &&
|
||||
azureDevOpsHostSet.trim() !== "" && (
|
||||
<KeyStatusIcon testId="ado-set-host-indicator" isSet />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<AzureDevOpsTokenHelpAnchor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
||||
import AzureDevOpsLogo from "#/assets/branding/azure-devops-logo.svg?react";
|
||||
import { useAuthUrl } from "#/hooks/use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
|
||||
@@ -24,11 +23,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
identityProvider: "gitlab",
|
||||
});
|
||||
|
||||
const azureDevOpsAuthUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
identityProvider: "azure_devops",
|
||||
});
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
@@ -43,13 +37,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAzureDevOpsAuth = () => {
|
||||
if (azureDevOpsAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = azureDevOpsAuthUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="border border-tertiary">
|
||||
@@ -80,17 +67,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
>
|
||||
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleAzureDevOpsAuth}
|
||||
className="w-full"
|
||||
startContent={<AzureDevOpsLogo width={20} height={20} />}
|
||||
>
|
||||
{t(I18nKey.AZURE_DEVOPS$CONNECT_TO_AZURE_DEVOPS) ||
|
||||
"Connect to Azure DevOps"}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useAutoLogin = () => {
|
||||
// Get the stored login method
|
||||
const loginMethod = getLoginMethod();
|
||||
|
||||
// Get the auth URLs for all providers
|
||||
// Get the auth URLs for both providers
|
||||
const githubAuthUrl = useAuthUrl({
|
||||
appMode: config?.APP_MODE || null,
|
||||
identityProvider: "github",
|
||||
@@ -26,11 +26,6 @@ export const useAutoLogin = () => {
|
||||
identityProvider: "gitlab",
|
||||
});
|
||||
|
||||
const azureDevOpsAuthUrl = useAuthUrl({
|
||||
appMode: config?.APP_MODE || null,
|
||||
identityProvider: "azure_devops",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Only auto-login in SAAS mode
|
||||
if (config?.APP_MODE !== "saas") {
|
||||
@@ -53,14 +48,8 @@ export const useAutoLogin = () => {
|
||||
}
|
||||
|
||||
// Get the appropriate auth URL based on the stored login method
|
||||
let authUrl: string | null = null;
|
||||
if (loginMethod === LoginMethod.GITHUB) {
|
||||
authUrl = githubAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.GITLAB) {
|
||||
authUrl = gitlabAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.AZURE_DEVOPS) {
|
||||
authUrl = azureDevOpsAuthUrl;
|
||||
}
|
||||
const authUrl =
|
||||
loginMethod === LoginMethod.GITHUB ? githubAuthUrl : gitlabAuthUrl;
|
||||
|
||||
// If we have an auth URL, redirect to it
|
||||
if (authUrl) {
|
||||
@@ -79,6 +68,5 @@ export const useAutoLogin = () => {
|
||||
loginMethod,
|
||||
githubAuthUrl,
|
||||
gitlabAuthUrl,
|
||||
azureDevOpsAuthUrl,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -508,16 +508,6 @@ export enum I18nKey {
|
||||
SETTINGS_FORM$BASE_URL = "SETTINGS_FORM$BASE_URL",
|
||||
GITHUB$CONNECT_TO_GITHUB = "GITHUB$CONNECT_TO_GITHUB",
|
||||
GITLAB$CONNECT_TO_GITLAB = "GITLAB$CONNECT_TO_GITLAB",
|
||||
AZURE_DEVOPS$CONNECT_TO_AZURE_DEVOPS = "AZURE_DEVOPS$CONNECT_TO_AZURE_DEVOPS",
|
||||
AZURE_DEVOPS$TOKEN_LABEL = "AZURE_DEVOPS$TOKEN_LABEL",
|
||||
AZURE_DEVOPS$HOST_LABEL = "AZURE_DEVOPS$HOST_LABEL",
|
||||
AZURE_DEVOPS$HOST_HELP_TEXT = "AZURE_DEVOPS$HOST_HELP_TEXT",
|
||||
AZURE_DEVOPS$HOST_REQUIRED_ERROR = "AZURE_DEVOPS$HOST_REQUIRED_ERROR",
|
||||
AZURE_DEVOPS$TOKEN_REQUIRED_ERROR = "AZURE_DEVOPS$TOKEN_REQUIRED_ERROR",
|
||||
AZURE_DEVOPS$GET_TOKEN = "AZURE_DEVOPS$GET_TOKEN",
|
||||
AZURE_DEVOPS$TOKEN_HELP_TEXT = "AZURE_DEVOPS$TOKEN_HELP_TEXT",
|
||||
AZURE_DEVOPS$TOKEN_LINK_TEXT = "AZURE_DEVOPS$TOKEN_LINK_TEXT",
|
||||
AZURE_DEVOPS$INSTRUCTIONS_LINK_TEXT = "AZURE_DEVOPS$INSTRUCTIONS_LINK_TEXT",
|
||||
AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER = "AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER",
|
||||
WAITLIST$JOIN_WAITLIST = "WAITLIST$JOIN_WAITLIST",
|
||||
ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS = "ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS",
|
||||
|
||||
@@ -8127,166 +8127,6 @@
|
||||
"tr": "GitLab'a bağlan",
|
||||
"uk": "Увійти за допомогою GitLab"
|
||||
},
|
||||
"AZURE_DEVOPS$CONNECT_TO_AZURE_DEVOPS": {
|
||||
"en": "Log in with Azure DevOps",
|
||||
"ja": "Azure DevOpsに接続",
|
||||
"zh-CN": "连接到Azure DevOps",
|
||||
"zh-TW": "連接到Azure DevOps",
|
||||
"ko-KR": "Azure DevOps에 연결",
|
||||
"de": "Mit Azure DevOps verbinden",
|
||||
"no": "Koble til Azure DevOps",
|
||||
"it": "Connetti a Azure DevOps",
|
||||
"pt": "Conectar ao Azure DevOps",
|
||||
"es": "Conectar a Azure DevOps",
|
||||
"ar": "الاتصال بـ Azure DevOps",
|
||||
"fr": "Se connecter à Azure DevOps",
|
||||
"tr": "Azure DevOps'a bağlan",
|
||||
"uk": "Увійти за допомогою Azure DevOps"
|
||||
},
|
||||
"AZURE_DEVOPS$TOKEN_LABEL": {
|
||||
"en": "Azure DevOps Token",
|
||||
"ja": "Azure DevOpsトークン",
|
||||
"zh-CN": "Azure DevOps令牌",
|
||||
"zh-TW": "Azure DevOps權杖",
|
||||
"ko-KR": "Azure DevOps 토큰",
|
||||
"no": "Azure DevOps-token",
|
||||
"it": "Token Azure DevOps",
|
||||
"pt": "Token do Azure DevOps",
|
||||
"es": "Token de Azure DevOps",
|
||||
"ar": "رمز Azure DevOps",
|
||||
"fr": "Jeton Azure DevOps",
|
||||
"tr": "Azure DevOps Token",
|
||||
"de": "Azure DevOps-Token",
|
||||
"uk": "Токен Azure DevOps"
|
||||
},
|
||||
"AZURE_DEVOPS$HOST_LABEL": {
|
||||
"en": "Azure DevOps Organization URL (Required)",
|
||||
"ja": "Azure DevOps組織URL(必須)",
|
||||
"zh-CN": "Azure DevOps组织URL(必需)",
|
||||
"zh-TW": "Azure DevOps組織URL(必需)",
|
||||
"ko-KR": "Azure DevOps 조직 URL (필수)",
|
||||
"no": "Azure DevOps organisasjons-URL (påkrevd)",
|
||||
"it": "URL organizzazione Azure DevOps (obbligatorio)",
|
||||
"pt": "URL da organização Azure DevOps (obrigatório)",
|
||||
"es": "URL de organización de Azure DevOps (requerido)",
|
||||
"ar": "رابط منظمة Azure DevOps (مطلوب)",
|
||||
"fr": "URL d'organisation Azure DevOps (requis)",
|
||||
"tr": "Azure DevOps Organizasyon URL'si (gerekli)",
|
||||
"de": "Azure DevOps-Organisations-URL (erforderlich)",
|
||||
"uk": "URL організації Azure DevOps (обов'язково)"
|
||||
},
|
||||
"AZURE_DEVOPS$HOST_HELP_TEXT": {
|
||||
"en": "Enter your organization URL (e.g., dev.azure.com/your-org). This is required because Azure DevOps tokens are organization-scoped.",
|
||||
"ja": "組織URL(例:dev.azure.com/your-org)を入力してください。Azure DevOpsトークンは組織スコープのため、これは必須です。",
|
||||
"zh-CN": "输入您的组织URL(例如:dev.azure.com/your-org)。这是必需的,因为Azure DevOps令牌是组织范围的。",
|
||||
"zh-TW": "輸入您的組織URL(例如:dev.azure.com/your-org)。這是必需的,因為Azure DevOps權杖是組織範圍的。",
|
||||
"ko-KR": "조직 URL을 입력하세요 (예: dev.azure.com/your-org). Azure DevOps 토큰이 조직 범위이므로 필수입니다.",
|
||||
"no": "Skriv inn organisasjons-URL (f.eks. dev.azure.com/your-org). Dette er påkrevd fordi Azure DevOps-tokens er organisasjonsbegrenset.",
|
||||
"it": "Inserisci l'URL della tua organizzazione (es. dev.azure.com/your-org). Questo è obbligatorio perché i token Azure DevOps sono limitati all'organizzazione.",
|
||||
"pt": "Digite a URL da sua organização (ex: dev.azure.com/your-org). Isso é obrigatório porque os tokens do Azure DevOps são limitados à organização.",
|
||||
"es": "Ingrese la URL de su organización (ej: dev.azure.com/your-org). Esto es requerido porque los tokens de Azure DevOps están limitados a la organización.",
|
||||
"ar": "أدخل رابط منظمتك (مثل: dev.azure.com/your-org). هذا مطلوب لأن رموز Azure DevOps محدودة النطاق للمنظمة.",
|
||||
"fr": "Entrez l'URL de votre organisation (ex: dev.azure.com/your-org). Ceci est requis car les jetons Azure DevOps sont limités à l'organisation.",
|
||||
"tr": "Organizasyon URL'nizi girin (örn: dev.azure.com/your-org). Azure DevOps tokenları organizasyon kapsamlı olduğu için bu gereklidir.",
|
||||
"de": "Geben Sie Ihre Organisations-URL ein (z.B. dev.azure.com/your-org). Dies ist erforderlich, da Azure DevOps-Token organisationsbezogen sind.",
|
||||
"uk": "Введіть URL вашої організації (наприклад: dev.azure.com/your-org). Це обов'язково, оскільки токени Azure DevOps обмежені організацією."
|
||||
},
|
||||
"AZURE_DEVOPS$HOST_REQUIRED_ERROR": {
|
||||
"en": "Organization URL is required when Azure DevOps token is provided.",
|
||||
"ja": "Azure DevOpsトークンが提供されている場合、組織URLが必要です。",
|
||||
"zh-CN": "提供Azure DevOps令牌时需要组织URL。",
|
||||
"zh-TW": "提供Azure DevOps權杖時需要組織URL。",
|
||||
"ko-KR": "Azure DevOps 토큰이 제공될 때 조직 URL이 필요합니다.",
|
||||
"no": "Organisasjons-URL kreves når Azure DevOps-token er oppgitt.",
|
||||
"it": "L'URL dell'organizzazione è richiesto quando viene fornito il token Azure DevOps.",
|
||||
"pt": "A URL da organização é necessária quando o token do Azure DevOps é fornecido.",
|
||||
"es": "Se requiere la URL de la organización cuando se proporciona el token de Azure DevOps.",
|
||||
"ar": "رابط المنظمة مطلوب عند توفير رمز Azure DevOps.",
|
||||
"fr": "L'URL d'organisation est requise lorsque le jeton Azure DevOps est fourni.",
|
||||
"tr": "Azure DevOps jetonu sağlandığında organizasyon URL'si gereklidir.",
|
||||
"de": "Organisations-URL ist erforderlich, wenn Azure DevOps-Token bereitgestellt wird.",
|
||||
"uk": "URL організації потрібен, коли надається токен Azure DevOps."
|
||||
},
|
||||
"AZURE_DEVOPS$TOKEN_REQUIRED_ERROR": {
|
||||
"en": "Azure DevOps token is required when organization URL is provided.",
|
||||
"ja": "組織URLが提供されている場合、Azure DevOpsトークンが必要です。",
|
||||
"zh-CN": "提供组织URL时需要Azure DevOps令牌。",
|
||||
"zh-TW": "提供組織URL時需要Azure DevOps權杖。",
|
||||
"ko-KR": "조직 URL이 제공될 때 Azure DevOps 토큰이 필요합니다.",
|
||||
"no": "Azure DevOps-token kreves når organisasjons-URL er oppgitt.",
|
||||
"it": "Il token Azure DevOps è richiesto quando viene fornito l'URL dell'organizzazione.",
|
||||
"pt": "O token do Azure DevOps é necessário quando a URL da organização é fornecida.",
|
||||
"es": "Se requiere el token de Azure DevOps cuando se proporciona la URL de la organización.",
|
||||
"ar": "رمز Azure DevOps مطلوب عند توفير رابط المنظمة.",
|
||||
"fr": "Le jeton Azure DevOps est requis lorsque l'URL d'organisation est fournie.",
|
||||
"tr": "Organizasyon URL'si sağlandığında Azure DevOps jetonu gereklidir.",
|
||||
"de": "Azure DevOps-Token ist erforderlich, wenn Organisations-URL bereitgestellt wird.",
|
||||
"uk": "Токен Azure DevOps потрібен, коли надається URL організації."
|
||||
},
|
||||
"AZURE_DEVOPS$GET_TOKEN": {
|
||||
"en": "Get an Azure DevOps token",
|
||||
"ja": "Azure DevOpsトークンを取得",
|
||||
"zh-CN": "获取Azure DevOps令牌",
|
||||
"zh-TW": "獲取Azure DevOps權杖",
|
||||
"ko-KR": "Azure DevOps 토큰 받기",
|
||||
"no": "Få et Azure DevOps-token",
|
||||
"it": "Ottieni un token Azure DevOps",
|
||||
"pt": "Obter um token do Azure DevOps",
|
||||
"es": "Obtener un token de Azure DevOps",
|
||||
"ar": "الحصول على رمز Azure DevOps",
|
||||
"fr": "Obtenir un jeton Azure DevOps",
|
||||
"tr": "Azure DevOps token al",
|
||||
"de": "Azure DevOps-Token erhalten",
|
||||
"uk": "Отримати токен Azure DevOps"
|
||||
},
|
||||
"AZURE_DEVOPS$TOKEN_HELP_TEXT": {
|
||||
"en": "Get your <0>Azure DevOps personal access token</0> or <1>click here for instructions</1>.",
|
||||
"ja": "<0>Azure DevOps個人アクセストークン</0>を取得するか、<1>手順についてはここをクリック</1>。",
|
||||
"zh-CN": "获取您的<0>Azure DevOps个人访问令牌</0>或<1>点击此处获取说明</1>。",
|
||||
"zh-TW": "取得您的<0>Azure DevOps個人存取權杖</0>或<1>點擊此處獲取說明</1>。",
|
||||
"ko-KR": "<0>Azure DevOps 개인 액세스 토큰</0>을 받거나 <1>지침을 보려면 여기를 클릭</1>하세요.",
|
||||
"no": "Få ditt <0>Azure DevOps personlige tilgangstoken</0> eller <1>klikk her for instruksjoner</1>.",
|
||||
"it": "Ottieni il tuo <0>token di accesso personale Azure DevOps</0> o <1>clicca qui per istruzioni</1>.",
|
||||
"pt": "Obtenha seu <0>token de acesso pessoal do Azure DevOps</0> ou <1>clique aqui para instruções</1>.",
|
||||
"es": "Obtenga su <0>token de acceso personal de Azure DevOps</0> o <1>haga clic aquí para obtener instrucciones</1>.",
|
||||
"ar": "احصل على <0>رمز الوصول الشخصي Azure DevOps</0> الخاص بك أو <1>انقر هنا للحصول على تعليمات</1>.",
|
||||
"fr": "Obtenez votre <0>jeton d'accès personnel Azure DevOps</0> ou <1>cliquez ici pour les instructions</1>.",
|
||||
"tr": "<0>Azure DevOps kişisel erişim jetonunuzu</0> alın veya <1>talimatlar için buraya tıklayın</1>.",
|
||||
"de": "Holen Sie sich Ihr <0>Azure DevOps Personal Access Token</0> oder <1>klicken Sie hier für Anweisungen</1>.",
|
||||
"uk": "Отримайте свій <0>особистий токен доступу Azure DevOps</0> або <1>натисніть тут, щоб отримати інструкції</1>."
|
||||
},
|
||||
"AZURE_DEVOPS$TOKEN_LINK_TEXT": {
|
||||
"en": "Azure DevOps personal access token",
|
||||
"ja": "Azure DevOps個人アクセストークン",
|
||||
"zh-CN": "Azure DevOps个人访问令牌",
|
||||
"zh-TW": "Azure DevOps個人存取權杖",
|
||||
"ko-KR": "Azure DevOps 개인 액세스 토큰",
|
||||
"no": "Azure DevOps personlige tilgangstoken",
|
||||
"it": "token di accesso personale Azure DevOps",
|
||||
"pt": "token de acesso pessoal do Azure DevOps",
|
||||
"es": "token de acceso personal de Azure DevOps",
|
||||
"ar": "رمز الوصول الشخصي Azure DevOps",
|
||||
"fr": "jeton d'accès personnel Azure DevOps",
|
||||
"tr": "Azure DevOps kişisel erişim jetonu",
|
||||
"de": "Azure DevOps Personal Access Token",
|
||||
"uk": "особистий токен доступу Azure DevOps"
|
||||
},
|
||||
"AZURE_DEVOPS$INSTRUCTIONS_LINK_TEXT": {
|
||||
"en": "click here for instructions",
|
||||
"ja": "手順についてはここをクリック",
|
||||
"zh-CN": "点击此处获取说明",
|
||||
"zh-TW": "點擊此處獲取說明",
|
||||
"ko-KR": "지침을 보려면 여기를 클릭",
|
||||
"no": "klikk her for instruksjoner",
|
||||
"it": "clicca qui per istruzioni",
|
||||
"pt": "clique aqui para instruções",
|
||||
"es": "haga clic aquí para obtener instrucciones",
|
||||
"ar": "انقر هنا للحصول على تعليمات",
|
||||
"fr": "cliquez ici pour les instructions",
|
||||
"tr": "talimatlar için buraya tıklayın",
|
||||
"de": "klicken Sie hier für Anweisungen",
|
||||
"uk": "натисніть тут, щоб отримати інструкції"
|
||||
},
|
||||
"AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER": {
|
||||
"en": "Log in to OpenHands",
|
||||
"ja": "IDプロバイダーでサインイン",
|
||||
|
||||
@@ -19,7 +19,6 @@ const chat = ws.link(`ws://${window?.location.host}/socket.io`);
|
||||
|
||||
export const handlers: WebSocketHandler[] = [
|
||||
chat.addEventListener("connection", (connection) => {
|
||||
// @ts-expect-error - MSW v2 type incompatibility
|
||||
const io = toSocketIo(connection);
|
||||
// @ts-expect-error - accessing private property for testing purposes
|
||||
const { url }: { url: URL } = io.client.connection;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { GitHubTokenInput } from "#/components/features/settings/git-settings/github-token-input";
|
||||
import { GitLabTokenInput } from "#/components/features/settings/git-settings/gitlab-token-input";
|
||||
import { AzureDevOpsTokenInput } from "#/components/features/settings/git-settings/azure-devops-token-input";
|
||||
import { ConfigureGitHubRepositoriesAnchor } from "#/components/features/settings/git-settings/configure-github-repositories-anchor";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
@@ -33,24 +32,18 @@ function GitSettingsScreen() {
|
||||
React.useState(false);
|
||||
const [gitlabTokenInputHasValue, setGitlabTokenInputHasValue] =
|
||||
React.useState(false);
|
||||
const [azureDevOpsTokenInputHasValue, setAzureDevOpsTokenInputHasValue] =
|
||||
React.useState(false);
|
||||
|
||||
const [githubHostInputHasValue, setGithubHostInputHasValue] =
|
||||
React.useState(false);
|
||||
const [gitlabHostInputHasValue, setGitlabHostInputHasValue] =
|
||||
React.useState(false);
|
||||
const [azureDevOpsHostInputHasValue, setAzureDevOpsHostInputHasValue] =
|
||||
React.useState(false);
|
||||
|
||||
const existingGithubHost = settings?.PROVIDER_TOKENS_SET.github;
|
||||
const existingGitlabHost = settings?.PROVIDER_TOKENS_SET.gitlab;
|
||||
const existingAzureDevOpsHost = settings?.PROVIDER_TOKENS_SET.azure_devops;
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const isGitHubTokenSet = providers.includes("github");
|
||||
const isGitLabTokenSet = providers.includes("gitlab");
|
||||
const isAzureDevOpsTokenSet = providers.includes("azure_devops");
|
||||
|
||||
const formAction = async (formData: FormData) => {
|
||||
const disconnectButtonClicked =
|
||||
@@ -63,33 +56,14 @@ function GitSettingsScreen() {
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString() || "";
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString() || "";
|
||||
const azureDevOpsToken =
|
||||
formData.get("azure-devops-token-input")?.toString() || "";
|
||||
const githubHost = formData.get("github-host-input")?.toString() || "";
|
||||
const gitlabHost = formData.get("gitlab-host-input")?.toString() || "";
|
||||
const azureDevOpsHost =
|
||||
formData.get("azure-devops-host-input")?.toString() || "";
|
||||
|
||||
// Validate Azure DevOps token and host dependency
|
||||
const hasAzureDevOpsToken = azureDevOpsToken.trim() !== "";
|
||||
const hasAzureDevOpsHost = azureDevOpsHost.trim() !== "";
|
||||
|
||||
if (hasAzureDevOpsToken && !hasAzureDevOpsHost) {
|
||||
displayErrorToast(t(I18nKey.AZURE_DEVOPS$HOST_REQUIRED_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAzureDevOpsHost && !hasAzureDevOpsToken) {
|
||||
displayErrorToast(t(I18nKey.AZURE_DEVOPS$TOKEN_REQUIRED_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
saveGitProviders(
|
||||
{
|
||||
providers: {
|
||||
github: { token: githubToken, host: githubHost },
|
||||
gitlab: { token: gitlabToken, host: gitlabHost },
|
||||
azure_devops: { token: azureDevOpsToken, host: azureDevOpsHost },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,10 +77,8 @@ function GitSettingsScreen() {
|
||||
onSettled: () => {
|
||||
setGithubTokenInputHasValue(false);
|
||||
setGitlabTokenInputHasValue(false);
|
||||
setAzureDevOpsTokenInputHasValue(false);
|
||||
setGithubHostInputHasValue(false);
|
||||
setGitlabHostInputHasValue(false);
|
||||
setAzureDevOpsHostInputHasValue(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -115,10 +87,8 @@ function GitSettingsScreen() {
|
||||
const formIsClean =
|
||||
!githubTokenInputHasValue &&
|
||||
!gitlabTokenInputHasValue &&
|
||||
!azureDevOpsTokenInputHasValue &&
|
||||
!githubHostInputHasValue &&
|
||||
!gitlabHostInputHasValue &&
|
||||
!azureDevOpsHostInputHasValue;
|
||||
!gitlabHostInputHasValue;
|
||||
const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG;
|
||||
|
||||
return (
|
||||
@@ -141,7 +111,7 @@ function GitSettingsScreen() {
|
||||
setGithubTokenInputHasValue(!!value);
|
||||
}}
|
||||
onGitHubHostChange={(value) => {
|
||||
setGithubHostInputHasValue(!!value);
|
||||
setGitlabHostInputHasValue(!!value);
|
||||
}}
|
||||
githubHostSet={existingGithubHost}
|
||||
/>
|
||||
@@ -160,20 +130,6 @@ function GitSettingsScreen() {
|
||||
gitlabHostSet={existingGitlabHost}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isSaas && (
|
||||
<AzureDevOpsTokenInput
|
||||
name="azure-devops-token-input"
|
||||
isAzureDevOpsTokenSet={isAzureDevOpsTokenSet}
|
||||
onChange={(value) => {
|
||||
setAzureDevOpsTokenInputHasValue(!!value);
|
||||
}}
|
||||
onAzureDevOpsHostChange={(value) => {
|
||||
setAzureDevOpsHostInputHasValue(!!value);
|
||||
}}
|
||||
azureDevOpsHostSet={existingAzureDevOpsHost}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -187,9 +143,7 @@ function GitSettingsScreen() {
|
||||
name="disconnect-tokens-button"
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
isDisabled={
|
||||
!isGitHubTokenSet && !isGitLabTokenSet && !isAzureDevOpsTokenSet
|
||||
}
|
||||
isDisabled={!isGitHubTokenSet && !isGitLabTokenSet}
|
||||
>
|
||||
Disconnect Tokens
|
||||
</BrandButton>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export const ProviderOptions = {
|
||||
github: "github",
|
||||
gitlab: "gitlab",
|
||||
azure_devops: "azure_devops",
|
||||
} as const;
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Generates a URL to redirect to for OAuth authentication
|
||||
* @param identityProvider The identity provider to use (e.g., "github", "gitlab", "azure_devops")
|
||||
* @param identityProvider The identity provider to use (e.g., "github", "gitlab")
|
||||
* @param requestUrl The URL of the request
|
||||
* @returns The URL to redirect to for OAuth
|
||||
*/
|
||||
|
||||
@@ -7,12 +7,11 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
export enum LoginMethod {
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
AZURE_DEVOPS = "azure_devops",
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the login method in local storage
|
||||
* @param method The login method (github, gitlab, or azure_devops)
|
||||
* @param method The login method (github or gitlab)
|
||||
*/
|
||||
export const setLoginMethod = (method: LoginMethod): void => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD, method);
|
||||
|
||||
@@ -19,10 +19,10 @@ vi.mock("react-i18next", () => ({
|
||||
|
||||
describe("RepositorySelectionForm", () => {
|
||||
const mockOnRepoSelection = vi.fn();
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
|
||||
// Mock the hooks with default values
|
||||
(useUserRepositories as any).mockReturnValue({
|
||||
data: [
|
||||
@@ -32,7 +32,7 @@ describe("RepositorySelectionForm", () => {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
|
||||
(useRepositoryBranches as any).mockReturnValue({
|
||||
data: [
|
||||
{ name: "main" },
|
||||
@@ -41,90 +41,90 @@ describe("RepositorySelectionForm", () => {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
|
||||
(useCreateConversation as any).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
});
|
||||
|
||||
|
||||
(useIsCreatingConversation as any).mockReturnValue(false);
|
||||
});
|
||||
|
||||
|
||||
it("should clear selected branch when input is empty", async () => {
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
|
||||
// First select a repository to enable the branch dropdown
|
||||
const repoDropdown = screen.getByTestId("repository-dropdown");
|
||||
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
|
||||
|
||||
|
||||
// Get the branch dropdown and verify it's enabled
|
||||
const branchDropdown = screen.getByTestId("branch-dropdown");
|
||||
expect(branchDropdown).not.toBeDisabled();
|
||||
|
||||
|
||||
// Simulate deleting all text in the branch input
|
||||
fireEvent.change(branchDropdown, { target: { value: "" } });
|
||||
|
||||
|
||||
// Verify the branch input is cleared (no selected branch)
|
||||
expect(branchDropdown).toHaveValue("");
|
||||
});
|
||||
|
||||
|
||||
it("should clear selected branch when input contains only whitespace", async () => {
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
|
||||
// First select a repository to enable the branch dropdown
|
||||
const repoDropdown = screen.getByTestId("repository-dropdown");
|
||||
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
|
||||
|
||||
|
||||
// Get the branch dropdown and verify it's enabled
|
||||
const branchDropdown = screen.getByTestId("branch-dropdown");
|
||||
expect(branchDropdown).not.toBeDisabled();
|
||||
|
||||
|
||||
// Simulate entering only whitespace in the branch input
|
||||
fireEvent.change(branchDropdown, { target: { value: " " } });
|
||||
|
||||
|
||||
// Verify the branch input is cleared (no selected branch)
|
||||
expect(branchDropdown).toHaveValue("");
|
||||
});
|
||||
|
||||
it("should keep branch empty after being cleared even with auto-selection", async () => {
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
|
||||
// First select a repository to enable the branch dropdown
|
||||
const repoDropdown = screen.getByTestId("repository-dropdown");
|
||||
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
|
||||
|
||||
|
||||
// Get the branch dropdown and verify it's enabled
|
||||
const branchDropdown = screen.getByTestId("branch-dropdown");
|
||||
expect(branchDropdown).not.toBeDisabled();
|
||||
|
||||
|
||||
// The branch should be auto-selected to "main" initially
|
||||
expect(branchDropdown).toHaveValue("main");
|
||||
|
||||
|
||||
// Simulate deleting all text in the branch input
|
||||
fireEvent.change(branchDropdown, { target: { value: "" } });
|
||||
|
||||
|
||||
// Verify the branch input is cleared (no selected branch)
|
||||
expect(branchDropdown).toHaveValue("");
|
||||
|
||||
|
||||
// Trigger a re-render by changing something else
|
||||
fireEvent.change(repoDropdown, { target: { value: "test/repo2" } });
|
||||
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
|
||||
|
||||
|
||||
// The branch should be auto-selected to "main" again after repo change
|
||||
expect(branchDropdown).toHaveValue("main");
|
||||
|
||||
|
||||
// Clear it again
|
||||
fireEvent.change(branchDropdown, { target: { value: "" } });
|
||||
|
||||
|
||||
// Verify it stays empty
|
||||
expect(branchDropdown).toHaveValue("");
|
||||
|
||||
|
||||
// Simulate a component update without changing repos
|
||||
// This would normally trigger the useEffect if our fix wasn't working
|
||||
fireEvent.blur(branchDropdown);
|
||||
|
||||
|
||||
// Verify it still stays empty
|
||||
expect(branchDropdown).toHaveValue("");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,188 +0,0 @@
|
||||
---
|
||||
name: azure_devops
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- azure devops
|
||||
- azure
|
||||
- devops
|
||||
---
|
||||
|
||||
<ROLE>
|
||||
You are an Azure DevOps expert who can help users interact with Azure DevOps repositories, work items, and pull requests.
|
||||
</ROLE>
|
||||
|
||||
<AZURE_DEVOPS_INTEGRATION>
|
||||
OpenHands supports Azure DevOps integration similar to GitHub and GitLab. You can use the `AZURE_DEVOPS_TOKEN` environment variable to authenticate with Azure DevOps.
|
||||
|
||||
## Authentication
|
||||
To use Azure DevOps with OpenHands, you need a Personal Access Token (PAT) with appropriate permissions:
|
||||
1. Go to your Azure DevOps organization settings
|
||||
2. Select "Personal access tokens"
|
||||
3. Create a new token with the following scopes:
|
||||
- Code (Read & Write)
|
||||
- Work Items (Read & Write)
|
||||
- Pull Request Threads (Read & Write)
|
||||
|
||||
## Repository Format
|
||||
When working with Azure DevOps repositories in OpenHands, use the following format:
|
||||
- Repository name: `project/repo`
|
||||
- Organization: Your Azure DevOps organization name
|
||||
|
||||
## Environment Variables
|
||||
- `AZURE_DEVOPS_TOKEN`: Your Azure DevOps Personal Access Token
|
||||
|
||||
## Common Operations
|
||||
- Clone a repository: `git clone https://dev.azure.com/organization/project/_git/repo`
|
||||
- Create a pull request: Use the Azure DevOps API or web interface
|
||||
- Work with issues: Azure DevOps uses work items instead of issues
|
||||
|
||||
## Azure DevOps API
|
||||
OpenHands uses the official Azure DevOps Python API to interact with Azure DevOps. The API is available at https://github.com/microsoft/azure-devops-python-api.
|
||||
|
||||
```python
|
||||
from azure.devops.connection import Connection
|
||||
from msrest.authentication import BasicAuthentication
|
||||
import os
|
||||
|
||||
# Authentication
|
||||
personal_access_token = os.environ.get('AZURE_DEVOPS_TOKEN')
|
||||
organization_url = 'https://dev.azure.com/your-organization'
|
||||
|
||||
# Create a connection
|
||||
credentials = BasicAuthentication('', personal_access_token)
|
||||
connection = Connection(base_url=organization_url, creds=credentials)
|
||||
|
||||
# Get clients
|
||||
git_client = connection.clients.get_git_client()
|
||||
work_item_client = connection.clients.get_work_item_tracking_client()
|
||||
|
||||
# Example: Get repositories
|
||||
repositories = git_client.get_repositories()
|
||||
for repo in repositories:
|
||||
print(f"{repo.name} - {repo.url}")
|
||||
|
||||
# Example: Get work items
|
||||
work_items = work_item_client.get_work_items(ids=[1, 2, 3])
|
||||
for work_item in work_items:
|
||||
print(f"{work_item.id} - {work_item.fields['System.Title']}")
|
||||
```
|
||||
</AZURE_DEVOPS_INTEGRATION>
|
||||
|
||||
<TROUBLESHOOTING>
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Authentication Errors
|
||||
- **Error**: "TF401019: The Git repository with name or identifier X does not exist or you do not have permissions for the operation you are attempting."
|
||||
- **Solution**: Check that your PAT has the correct permissions and that you're using the correct organization, project, and repository names.
|
||||
|
||||
### Repository Format
|
||||
- **Error**: "Invalid repository name format: X. Expected format: project/repo"
|
||||
- **Solution**: Make sure you're using the correct format for repository names: `project/repo`.
|
||||
|
||||
### API Limitations
|
||||
- Azure DevOps API has rate limits. If you encounter rate limit errors, add delays between API calls.
|
||||
- Some operations may require additional permissions beyond what's listed above.
|
||||
|
||||
### Work Item Types
|
||||
- Azure DevOps uses different work item types (Bug, Task, User Story, etc.) instead of the Issue concept in GitHub/GitLab.
|
||||
- When working with work items, make sure to specify the correct work item type.
|
||||
</TROUBLESHOOTING>
|
||||
|
||||
<BEST_PRACTICES>
|
||||
## Best Practices for Azure DevOps
|
||||
|
||||
### Repository Structure
|
||||
- Use a clear branching strategy (e.g., GitFlow, trunk-based development)
|
||||
- Protect your main branch with branch policies
|
||||
|
||||
### Pull Requests
|
||||
- Use descriptive titles and descriptions
|
||||
- Link work items to pull requests
|
||||
- Use the "Squash merge" option to keep history clean
|
||||
|
||||
### Work Items
|
||||
- Use the appropriate work item type for each task
|
||||
- Maintain a clear hierarchy of work items
|
||||
- Use tags for better organization
|
||||
|
||||
### CI/CD Pipelines
|
||||
- Store pipeline definitions as YAML in your repository
|
||||
- Use templates for common tasks
|
||||
- Leverage variable groups for secrets management
|
||||
</BEST_PRACTICES>
|
||||
|
||||
<EXAMPLES>
|
||||
## Example Commands
|
||||
|
||||
### Clone a Repository
|
||||
```bash
|
||||
git clone https://dev.azure.com/organization/project/_git/repo
|
||||
```
|
||||
|
||||
### Create a Branch
|
||||
```bash
|
||||
git checkout -b feature/new-feature
|
||||
```
|
||||
|
||||
### Push Changes
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add new feature"
|
||||
git push -u origin feature/new-feature
|
||||
```
|
||||
|
||||
### Create a Pull Request (using API)
|
||||
```python
|
||||
from azure.devops.connection import Connection
|
||||
from msrest.authentication import BasicAuthentication
|
||||
import os
|
||||
|
||||
# Authentication
|
||||
personal_access_token = os.environ.get('AZURE_DEVOPS_TOKEN')
|
||||
organization_url = 'https://dev.azure.com/your-organization'
|
||||
|
||||
# Create a connection
|
||||
credentials = BasicAuthentication('', personal_access_token)
|
||||
connection = Connection(base_url=organization_url, creds=credentials)
|
||||
|
||||
# Get Git client
|
||||
git_client = connection.clients.get_git_client()
|
||||
|
||||
# Create pull request
|
||||
pr = git_client.create_pull_request(
|
||||
git_pull_request={
|
||||
'source_ref_name': 'refs/heads/feature/new-feature',
|
||||
'target_ref_name': 'refs/heads/main',
|
||||
'title': 'Add new feature',
|
||||
'description': 'This PR adds a new feature'
|
||||
},
|
||||
repository_id='repository-id',
|
||||
project='project-name'
|
||||
)
|
||||
```
|
||||
|
||||
### Get Work Items
|
||||
```python
|
||||
from azure.devops.connection import Connection
|
||||
from msrest.authentication import BasicAuthentication
|
||||
import os
|
||||
|
||||
# Authentication
|
||||
personal_access_token = os.environ.get('AZURE_DEVOPS_TOKEN')
|
||||
organization_url = 'https://dev.azure.com/your-organization'
|
||||
|
||||
# Create a connection
|
||||
credentials = BasicAuthentication('', personal_access_token)
|
||||
connection = Connection(base_url=organization_url, creds=credentials)
|
||||
|
||||
# Get Work Item Tracking client
|
||||
wit_client = connection.clients.get_work_item_tracking_client()
|
||||
|
||||
# Get work items
|
||||
work_items = wit_client.get_work_items(ids=[1, 2, 3])
|
||||
for work_item in work_items:
|
||||
print(f"{work_item.id} - {work_item.fields['System.Title']}")
|
||||
```
|
||||
</EXAMPLES>
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
triggers:
|
||||
- /codereview
|
||||
---
|
||||
|
||||
PERSONA:
|
||||
You are an expert software engineer and code reviewer with deep experience in modern programming best practices, secure coding, and clean code principles.
|
||||
|
||||
TASK:
|
||||
Review the code changes in this pull request or merge request, and provide actionable feedback to help the author improve code quality, maintainability, and security. DO NOT modify the code; only provide specific feedback.
|
||||
|
||||
CONTEXT:
|
||||
You have full context of the code being committed in the pull request or merge request, including the diff, surrounding files, and project structure. The code is written in a modern language and follows typical idioms and patterns for that language.
|
||||
|
||||
ROLE:
|
||||
As an automated reviewer, your role is to analyze the code changes and produce structured comments, including line numbers, across the following scenarios:
|
||||
|
||||
CODE REVIEW SCENARIOS:
|
||||
1. Style and Formatting
|
||||
Check for:
|
||||
- Inconsistent indentation, spacing, or bracket usage
|
||||
- Unused imports or variables
|
||||
- Non-standard naming conventions
|
||||
- Missing or misformatted comments/docstrings
|
||||
- Violations of common language-specific style guides (e.g., PEP8, Google Style Guide)
|
||||
|
||||
2. Clarity and Readability
|
||||
Identify:
|
||||
- Overly complex or deeply nested logic
|
||||
- Functions doing too much (violating single responsibility)
|
||||
- Poor naming that obscures intent
|
||||
- Missing inline documentation for non-obvious logic
|
||||
|
||||
3. Security and Common Bug Patterns
|
||||
Watch for:
|
||||
- Unsanitized user input (e.g., in SQL, shell, or web contexts)
|
||||
- Hardcoded secrets or credentials
|
||||
- Incorrect use of cryptographic libraries
|
||||
- Common pitfalls (null dereferencing, off-by-one errors, race conditions)
|
||||
|
||||
INSTRUCTIONS FOR RESPONSE:
|
||||
Group the feedback by the scenarios above.
|
||||
|
||||
Then, for each issue you find:
|
||||
- Provide a line number or line range
|
||||
- Briefly explain why it's an issue
|
||||
- Suggest a concrete improvement
|
||||
|
||||
Use the following structure in your output:
|
||||
[Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
|
||||
[Lines 78–85] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
|
||||
[Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
|
||||
|
||||
REMEMBER, DO NOT MODIFY THE CODE. ONLY PROVIDE FEEDBACK IN YOUR RESPONSE.
|
||||
@@ -208,9 +208,7 @@ Note:
|
||||
# for visualwebarena, webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
|
||||
# initialize and retrieve the first observation by issuing an noop OP
|
||||
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions='noop(1000)', return_axtree=True
|
||||
)
|
||||
return BrowseInteractiveAction(browser_actions='noop(1000)', return_axtree=True)
|
||||
|
||||
for event in state.view:
|
||||
if isinstance(event, BrowseInteractiveAction):
|
||||
|
||||
@@ -54,7 +54,6 @@ class MCPStdioServerConfig(BaseModel):
|
||||
and set(self.env.items()) == set(other.env.items())
|
||||
)
|
||||
|
||||
|
||||
class MCPSHTTPServerConfig(BaseModel):
|
||||
url: str
|
||||
api_key: str | None = None
|
||||
|
||||
@@ -76,10 +76,27 @@ class OpenHandsConfig(BaseModel):
|
||||
)
|
||||
|
||||
# Deprecated parameters - will be removed in a future version
|
||||
workspace_base: str | None = Field(default=None, deprecated=True)
|
||||
workspace_mount_path: str | None = Field(default=None, deprecated=True)
|
||||
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
|
||||
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
|
||||
# Use sandbox.volumes instead, e.g. '/host/path:/workspace:rw'
|
||||
workspace_base: str | None = Field(
|
||||
default=None,
|
||||
deprecated=True,
|
||||
description="DEPRECATED: Use sandbox.volumes instead, e.g. '/host/path:/workspace:rw'",
|
||||
)
|
||||
workspace_mount_path: str | None = Field(
|
||||
default=None,
|
||||
deprecated=True,
|
||||
description="DEPRECATED: Use sandbox.volumes instead, e.g. '/host/path:/workspace:rw'",
|
||||
)
|
||||
workspace_mount_path_in_sandbox: str = Field(
|
||||
default='/workspace',
|
||||
deprecated=True,
|
||||
description="DEPRECATED: Use sandbox.volumes instead, e.g. '/host/path:/workspace:rw'",
|
||||
)
|
||||
workspace_mount_rewrite: str | None = Field(
|
||||
default=None,
|
||||
deprecated=True,
|
||||
description="DEPRECATED: Use sandbox.volumes instead, e.g. '/host/path:/workspace:rw'",
|
||||
)
|
||||
# End of deprecated parameters
|
||||
|
||||
cache_dir: str = Field(default='/tmp/cache')
|
||||
|
||||
@@ -85,7 +85,7 @@ class SandboxConfig(BaseModel):
|
||||
vscode_port: int | None = Field(default=None)
|
||||
volumes: str | None = Field(
|
||||
default=None,
|
||||
description="Volume mounts in the format 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'. Multiple mounts can be specified using commas, e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'",
|
||||
description="Volume mounts in the format 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'. Multiple mounts can be specified using commas, e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'. This replaces the deprecated workspace_base, workspace_mount_path, workspace_mount_path_in_sandbox, and workspace_mount_rewrite variables.",
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
@@ -307,8 +307,9 @@ def finalize_config(cfg: OpenHandsConfig) -> None:
|
||||
# Handle the sandbox.volumes parameter
|
||||
if cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
|
||||
logger.openhands_logger.warning(
|
||||
'DEPRECATED: The WORKSPACE_BASE and WORKSPACE_MOUNT_PATH environment variables are deprecated. '
|
||||
"Please use RUNTIME_MOUNT instead, e.g. 'RUNTIME_MOUNT=/my/host/dir:/workspace:rw'"
|
||||
'DEPRECATED: The workspace_base, workspace_mount_path, workspace_mount_path_in_sandbox, and workspace_mount_rewrite '
|
||||
'variables are deprecated and will be removed in a future version. '
|
||||
"Please use sandbox.volumes instead, e.g. 'SANDBOX_VOLUMES=/my/host/dir:/workspace:rw'"
|
||||
)
|
||||
if cfg.sandbox.volumes is not None:
|
||||
# Split by commas to handle multiple mounts
|
||||
|
||||
@@ -107,13 +107,6 @@ def initialize_repository_for_runtime(
|
||||
gitlab_token = SecretStr(os.environ['GITLAB_TOKEN'])
|
||||
provider_tokens[ProviderType.GITLAB] = ProviderToken(token=gitlab_token)
|
||||
|
||||
if 'AZURE_DEVOPS_TOKEN' in os.environ:
|
||||
azure_devops_token = SecretStr(os.environ['AZURE_DEVOPS_TOKEN'])
|
||||
azure_devops_host = os.environ.get('AZURE_DEVOPS_HOST')
|
||||
provider_tokens[ProviderType.AZURE_DEVOPS] = ProviderToken(
|
||||
token=azure_devops_token, host=azure_devops_host
|
||||
)
|
||||
|
||||
secret_store = (
|
||||
UserSecrets(provider_tokens=provider_tokens) if provider_tokens else None
|
||||
)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Azure DevOps integration package.
|
||||
"""
|
||||
@@ -1,801 +0,0 @@
|
||||
"""Azure DevOps service implementation using standard HTTP API calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
BaseGitService,
|
||||
Branch,
|
||||
ProviderType,
|
||||
Repository,
|
||||
RequestMethod,
|
||||
SuggestedTask,
|
||||
TaskType,
|
||||
UnknownException,
|
||||
User,
|
||||
)
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
class AzureDevOpsServiceImpl(BaseGitService):
|
||||
"""Azure DevOps service implementation using standard HTTP API calls."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str | None = None,
|
||||
token: SecretStr | None = None,
|
||||
external_auth_id: str | None = None,
|
||||
external_auth_token: SecretStr | None = None,
|
||||
external_token_manager: bool = False,
|
||||
base_domain: str | None = None,
|
||||
):
|
||||
"""Initialize the Azure DevOps service.
|
||||
|
||||
Args:
|
||||
user_id: The user ID
|
||||
token: The Azure DevOps personal access token
|
||||
external_auth_id: External auth ID (not used for Azure DevOps)
|
||||
external_auth_token: External auth token (not used for Azure DevOps)
|
||||
external_token_manager: Whether to use external token manager (not used for Azure DevOps)
|
||||
base_domain: The Azure DevOps organization URL (e.g., https://dev.azure.com/organization)
|
||||
"""
|
||||
self.user_id = user_id
|
||||
self.token = token
|
||||
self.external_auth_id = external_auth_id
|
||||
self.external_auth_token = external_auth_token
|
||||
self.external_token_manager = external_token_manager
|
||||
self.organization_url = base_domain or 'https://dev.azure.com'
|
||||
|
||||
# Extract organization name from URL for API calls
|
||||
if self.organization_url.startswith('https://dev.azure.com/'):
|
||||
self.organization = self.organization_url.replace(
|
||||
'https://dev.azure.com/', ''
|
||||
).rstrip('/')
|
||||
else:
|
||||
# Handle custom Azure DevOps Server URLs
|
||||
self.organization = (
|
||||
self.organization_url.split('/')[-1]
|
||||
if '/' in self.organization_url
|
||||
else self.organization_url
|
||||
)
|
||||
|
||||
self.base_url = f'https://dev.azure.com/{self.organization}/_apis'
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return ProviderType.AZURE_DEVOPS.value
|
||||
|
||||
async def _get_azure_devops_headers(self) -> dict[str, str]:
|
||||
"""Get headers for Azure DevOps API requests."""
|
||||
if not self.token:
|
||||
self.token = await self.get_latest_token()
|
||||
|
||||
if not self.token:
|
||||
raise AuthenticationError('No Azure DevOps token provided')
|
||||
|
||||
# Azure DevOps uses Basic authentication with PAT
|
||||
# Username can be empty, password is the PAT
|
||||
credentials = base64.b64encode(
|
||||
f':{self.token.get_secret_value()}'.encode()
|
||||
).decode()
|
||||
|
||||
return {
|
||||
'Authorization': f'Basic {credentials}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
def _has_token_expired(self, status_code: int) -> bool:
|
||||
"""Check if the token has expired."""
|
||||
return status_code == 401
|
||||
|
||||
async def execute_request(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
url: str,
|
||||
headers: dict,
|
||||
params: dict | None,
|
||||
method: RequestMethod = RequestMethod.GET,
|
||||
) -> httpx.Response:
|
||||
"""Execute an HTTP request."""
|
||||
if method == RequestMethod.GET:
|
||||
response = await client.get(url, headers=headers, params=params)
|
||||
elif method == RequestMethod.POST:
|
||||
# For Azure DevOps, we need to handle the case where params contains both
|
||||
# query parameters and JSON data. We'll use a special key to separate them.
|
||||
json_data = params.pop('_json_data', None) if params else None
|
||||
response = await client.post(
|
||||
url, headers=headers, params=params, json=json_data
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported HTTP method: {method}')
|
||||
|
||||
return response
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
url: str,
|
||||
params: dict | None = None,
|
||||
method: RequestMethod = RequestMethod.GET,
|
||||
json_data: dict | None = None,
|
||||
) -> tuple[Any, dict]:
|
||||
"""Make a request to the Azure DevOps API."""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
azure_devops_headers = await self._get_azure_devops_headers()
|
||||
|
||||
# Make initial request
|
||||
# For POST requests, embed json_data in params using special key
|
||||
if method == RequestMethod.POST and json_data is not None:
|
||||
if params is None:
|
||||
params = {}
|
||||
params['_json_data'] = json_data
|
||||
|
||||
response = await self.execute_request(
|
||||
client=client,
|
||||
url=url,
|
||||
headers=azure_devops_headers,
|
||||
params=params,
|
||||
method=method,
|
||||
)
|
||||
|
||||
# Handle token refresh if needed
|
||||
if self._has_token_expired(response.status_code):
|
||||
logger.warning('Azure DevOps token expired, attempting refresh')
|
||||
# For Azure DevOps, we don't have automatic token refresh
|
||||
# The user needs to provide a new PAT
|
||||
raise AuthenticationError(
|
||||
'Azure DevOps token expired. Please provide a new Personal Access Token.'
|
||||
)
|
||||
|
||||
if response.status_code >= 400:
|
||||
logger.error(
|
||||
f'Azure DevOps API error: {response.status_code} - {response.text}'
|
||||
)
|
||||
if response.status_code == 401:
|
||||
raise AuthenticationError(
|
||||
'Authentication failed with Azure DevOps'
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
raise AuthenticationError(
|
||||
'Access forbidden. Check your Azure DevOps permissions.'
|
||||
)
|
||||
elif response.status_code == 404:
|
||||
raise ValueError('Resource not found')
|
||||
else:
|
||||
raise UnknownException(
|
||||
f'Azure DevOps API error: {response.status_code}'
|
||||
)
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
except Exception:
|
||||
response_data = response.text
|
||||
|
||||
return response_data, {}
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Request error: {e}')
|
||||
raise UnknownException(f'Request failed: {e}')
|
||||
except Exception as e:
|
||||
logger.error(f'Unexpected error: {e}')
|
||||
raise UnknownException(f'Unexpected error: {e}')
|
||||
|
||||
async def get_latest_token(self) -> SecretStr | None:
|
||||
"""Get the latest token.
|
||||
|
||||
Returns:
|
||||
The latest token
|
||||
"""
|
||||
return self.token
|
||||
|
||||
async def get_user(self) -> User:
|
||||
"""Get the authenticated user.
|
||||
|
||||
Returns:
|
||||
The authenticated user
|
||||
"""
|
||||
try:
|
||||
# Try to get user profile from Azure DevOps
|
||||
# For organization-scoped tokens, we'll use the projects API to verify authentication
|
||||
# since the global profile API requires "All accessible organizations" scope
|
||||
|
||||
# Fallback: Try to get projects to verify authentication
|
||||
projects_url = f'{self.base_url}/projects'
|
||||
projects_params = {'api-version': '7.1-preview.4'}
|
||||
|
||||
projects_data, _ = await self._make_request(
|
||||
projects_url, params=projects_params
|
||||
)
|
||||
|
||||
# If we can get projects, authentication is working
|
||||
if projects_data:
|
||||
# Try to get connection data for more user info
|
||||
try:
|
||||
connection_url = f'{self.base_url}/connectionData'
|
||||
connection_params = {'api-version': '7.1-preview.1'}
|
||||
connection_data, _ = await self._make_request(
|
||||
connection_url, params=connection_params
|
||||
)
|
||||
|
||||
if connection_data and isinstance(connection_data, dict):
|
||||
auth_user = connection_data.get('authenticatedUser', {})
|
||||
return User(
|
||||
id=auth_user.get('id', 0),
|
||||
login=auth_user.get(
|
||||
'uniqueName', self.user_id or 'azure_devops_user'
|
||||
),
|
||||
avatar_url=auth_user.get('imageUrl', ''),
|
||||
name=auth_user.get(
|
||||
'displayName', self.user_id or 'Azure DevOps User'
|
||||
),
|
||||
email=auth_user.get('uniqueName'),
|
||||
company=None,
|
||||
)
|
||||
except Exception as connection_error:
|
||||
logger.debug(f'Could not get connection data: {connection_error}')
|
||||
|
||||
# Basic fallback if connection data fails
|
||||
return User(
|
||||
id=0, # Placeholder ID
|
||||
login=self.user_id or 'azure_devops_user',
|
||||
avatar_url='',
|
||||
name=self.user_id or 'Azure DevOps User',
|
||||
email=None,
|
||||
company=None,
|
||||
)
|
||||
|
||||
# If projects API also fails, try the old profile approach as last resort
|
||||
profile_url = f'{self.base_url}/profile/profiles/me'
|
||||
profile_params = {'api-version': '7.1-preview.3'}
|
||||
|
||||
try:
|
||||
profile_data, _ = await self._make_request(
|
||||
profile_url, params=profile_params
|
||||
)
|
||||
|
||||
if profile_data and isinstance(profile_data, dict):
|
||||
return User(
|
||||
id=profile_data.get('id', 0),
|
||||
login=profile_data.get(
|
||||
'emailAddress', self.user_id or 'azure_devops_user'
|
||||
),
|
||||
avatar_url=profile_data.get('avatar', {}).get('value', ''),
|
||||
name=profile_data.get(
|
||||
'displayName', self.user_id or 'Azure DevOps User'
|
||||
),
|
||||
email=profile_data.get('emailAddress'),
|
||||
company=None,
|
||||
)
|
||||
except Exception as profile_error:
|
||||
logger.warning(f'Could not get user profile: {profile_error}')
|
||||
raise AuthenticationError('Failed to authenticate with Azure DevOps')
|
||||
|
||||
except AuthenticationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting Azure DevOps user: {e}')
|
||||
raise AuthenticationError(f'Failed to authenticate with Azure DevOps: {e}')
|
||||
|
||||
# This should never be reached, but added for mypy
|
||||
raise AuthenticationError('Failed to authenticate with Azure DevOps')
|
||||
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
"""Get repositories for the authenticated user.
|
||||
|
||||
Args:
|
||||
sort: The sort order
|
||||
app_mode: The app mode
|
||||
|
||||
Returns:
|
||||
A list of repositories
|
||||
"""
|
||||
try:
|
||||
# Get all repositories across all projects
|
||||
repos_url = f'{self.base_url}/git/repositories'
|
||||
repos_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
repos_data, _ = await self._make_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
return []
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
|
||||
# Convert to Repository objects
|
||||
result = []
|
||||
for repo in repositories:
|
||||
project_name = repo.get('project', {}).get('name', 'Unknown')
|
||||
repo_name = repo.get('name', 'Unknown')
|
||||
|
||||
result.append(
|
||||
Repository(
|
||||
id=repo.get('id', ''),
|
||||
full_name=f'{project_name}/{repo_name}',
|
||||
git_provider=ProviderType.AZURE_DEVOPS,
|
||||
is_public=False, # Azure DevOps repos are private by default
|
||||
stargazers_count=None,
|
||||
link_header=None,
|
||||
pushed_at=None,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting Azure DevOps repositories: {e}')
|
||||
return []
|
||||
|
||||
async def search_repositories(
|
||||
self,
|
||||
query: str,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
) -> list[Repository]:
|
||||
"""Search for repositories.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
per_page: The number of results per page
|
||||
sort: The sort order
|
||||
order: The sort direction
|
||||
|
||||
Returns:
|
||||
A list of repositories
|
||||
"""
|
||||
try:
|
||||
# Get all repositories (Azure DevOps doesn't have a search API for repos)
|
||||
repos_url = f'{self.base_url}/git/repositories'
|
||||
repos_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
repos_data, _ = await self._make_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
return []
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
|
||||
# Filter repositories by name (simple client-side filtering)
|
||||
filtered_repos = [
|
||||
repo
|
||||
for repo in repositories
|
||||
if query.lower() in repo.get('name', '').lower()
|
||||
or query.lower() in repo.get('project', {}).get('name', '').lower()
|
||||
]
|
||||
|
||||
# Convert to Repository objects
|
||||
result = []
|
||||
for repo in filtered_repos[:per_page]:
|
||||
project_name = repo.get('project', {}).get('name', 'Unknown')
|
||||
repo_name = repo.get('name', 'Unknown')
|
||||
|
||||
result.append(
|
||||
Repository(
|
||||
id=repo.get('id', ''),
|
||||
full_name=f'{project_name}/{repo_name}',
|
||||
git_provider=ProviderType.AZURE_DEVOPS,
|
||||
is_public=False, # Azure DevOps repos are private by default
|
||||
stargazers_count=None,
|
||||
link_header=None,
|
||||
pushed_at=None,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'Error searching Azure DevOps repositories: {e}')
|
||||
return []
|
||||
|
||||
async def get_suggested_tasks(self) -> list[SuggestedTask]:
|
||||
"""Get suggested tasks for the authenticated user.
|
||||
|
||||
Returns:
|
||||
A list of suggested tasks including:
|
||||
- Open issues assigned to the user
|
||||
- Pull requests authored by the user with:
|
||||
- Merge conflicts
|
||||
- Failing checks
|
||||
- Unresolved comments
|
||||
"""
|
||||
tasks: list[SuggestedTask] = []
|
||||
|
||||
try:
|
||||
# Get open work items (bugs/issues)
|
||||
await self._get_work_item_tasks(tasks)
|
||||
|
||||
# Get pull request tasks
|
||||
await self._get_pull_request_tasks(tasks)
|
||||
|
||||
return tasks
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting Azure DevOps suggested tasks: {e}')
|
||||
return []
|
||||
|
||||
async def _get_work_item_tasks(self, tasks: list[SuggestedTask]) -> None:
|
||||
"""Get work item tasks using WIQL query."""
|
||||
try:
|
||||
# Use WIQL to query for open bugs
|
||||
wiql_url = f'{self.base_url}/wit/wiql'
|
||||
wiql_params = {'api-version': '7.1-preview.2'}
|
||||
|
||||
wiql_query = {
|
||||
'query': """
|
||||
select [System.Id],
|
||||
[System.WorkItemType],
|
||||
[System.Title],
|
||||
[System.State],
|
||||
[System.TeamProject]
|
||||
from WorkItems
|
||||
where [System.WorkItemType] in ('Bug', 'Issue', 'Task')
|
||||
and [System.State] <> 'Closed'
|
||||
and [System.State] <> 'Resolved'
|
||||
and [System.State] <> 'Done'
|
||||
order by [System.ChangedDate] desc
|
||||
"""
|
||||
}
|
||||
|
||||
wiql_data, _ = await self._make_request(
|
||||
wiql_url,
|
||||
params=wiql_params,
|
||||
method=RequestMethod.POST,
|
||||
json_data=wiql_query,
|
||||
)
|
||||
|
||||
if not wiql_data or not isinstance(wiql_data, dict):
|
||||
return
|
||||
|
||||
work_items = wiql_data.get('workItems', [])[:10] # Limit to 10
|
||||
|
||||
# Get full work item details
|
||||
for work_item in work_items:
|
||||
work_item_id = work_item.get('id')
|
||||
if not work_item_id:
|
||||
continue
|
||||
|
||||
# Get work item details
|
||||
work_item_url = f'{self.base_url}/wit/workitems/{work_item_id}'
|
||||
work_item_params = {'api-version': '7.1-preview.3'}
|
||||
|
||||
work_item_data, _ = await self._make_request(
|
||||
work_item_url, params=work_item_params
|
||||
)
|
||||
|
||||
if work_item_data and isinstance(work_item_data, dict):
|
||||
fields = work_item_data.get('fields', {})
|
||||
project_name = fields.get('System.TeamProject', '')
|
||||
|
||||
tasks.append(
|
||||
SuggestedTask(
|
||||
git_provider=ProviderType.AZURE_DEVOPS,
|
||||
task_type=TaskType.OPEN_ISSUE,
|
||||
repo=project_name,
|
||||
issue_number=work_item_id,
|
||||
title=fields.get('System.Title', ''),
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Error getting work item tasks: {e}')
|
||||
|
||||
async def _get_pull_request_tasks(self, tasks: list[SuggestedTask]) -> None:
|
||||
"""Get pull request tasks."""
|
||||
try:
|
||||
# Get all repositories
|
||||
repos_url = f'{self.base_url}/git/repositories'
|
||||
repos_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
repos_data, _ = await self._make_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
return
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
|
||||
# For each repository, get pull requests
|
||||
for repo in repositories:
|
||||
project_name = repo.get('project', {}).get('name', '')
|
||||
repo_name = repo.get('name', '')
|
||||
repo_id = repo.get('id', '')
|
||||
full_repo_name = f'{project_name}/{repo_name}'
|
||||
|
||||
if not project_name or not repo_id:
|
||||
continue
|
||||
|
||||
# Get active pull requests
|
||||
prs_url = f'{self.base_url}/git/repositories/{repo_id}/pullrequests'
|
||||
prs_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'searchCriteria.status': 'active',
|
||||
}
|
||||
|
||||
prs_data, _ = await self._make_request(prs_url, params=prs_params)
|
||||
|
||||
if not prs_data or not isinstance(prs_data, dict):
|
||||
continue
|
||||
|
||||
pull_requests = prs_data.get('value', [])
|
||||
|
||||
for pr in pull_requests:
|
||||
pr_id = pr.get('pullRequestId')
|
||||
if not pr_id:
|
||||
continue
|
||||
|
||||
task_type = None
|
||||
|
||||
# Check for merge conflicts
|
||||
if pr.get('mergeStatus') == 'conflicts':
|
||||
task_type = TaskType.MERGE_CONFLICTS
|
||||
else:
|
||||
# Check for failing policy evaluations
|
||||
try:
|
||||
policy_url = f'{self.base_url}/policy/evaluations'
|
||||
policy_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'artifactId': f'vstfs:///CodeReview/CodeReviewId/{project_name}/{pr_id}',
|
||||
}
|
||||
|
||||
policy_data, _ = await self._make_request(
|
||||
policy_url, params=policy_params
|
||||
)
|
||||
|
||||
if policy_data and isinstance(policy_data, dict):
|
||||
evaluations = policy_data.get('value', [])
|
||||
has_failing_checks = any(
|
||||
eval.get('status') == 'rejected'
|
||||
for eval in evaluations
|
||||
)
|
||||
|
||||
if has_failing_checks:
|
||||
task_type = TaskType.FAILING_CHECKS
|
||||
except Exception:
|
||||
# Policy evaluations might not be accessible, continue
|
||||
pass
|
||||
|
||||
# Check for unresolved comments if no other issues found
|
||||
if not task_type:
|
||||
try:
|
||||
threads_url = f'{self.base_url}/git/repositories/{repo_id}/pullRequests/{pr_id}/threads'
|
||||
threads_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
threads_data, _ = await self._make_request(
|
||||
threads_url, params=threads_params
|
||||
)
|
||||
|
||||
if threads_data and isinstance(threads_data, dict):
|
||||
threads = threads_data.get('value', [])
|
||||
has_unresolved_comments = any(
|
||||
thread.get('status') == 'active'
|
||||
and not thread.get('isDeleted', False)
|
||||
for thread in threads
|
||||
)
|
||||
|
||||
if has_unresolved_comments:
|
||||
task_type = TaskType.UNRESOLVED_COMMENTS
|
||||
except Exception:
|
||||
# Threads might not be accessible, continue
|
||||
pass
|
||||
|
||||
# Add the task if we identified a specific issue
|
||||
if task_type:
|
||||
tasks.append(
|
||||
SuggestedTask(
|
||||
git_provider=ProviderType.AZURE_DEVOPS,
|
||||
task_type=task_type,
|
||||
repo=full_repo_name,
|
||||
issue_number=pr_id,
|
||||
title=pr.get('title', ''),
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Error getting pull request tasks: {e}')
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
) -> Repository:
|
||||
"""Get repository details from repository name.
|
||||
|
||||
Args:
|
||||
repository: The repository name (format: project/repo)
|
||||
|
||||
Returns:
|
||||
The repository details
|
||||
"""
|
||||
try:
|
||||
# Parse the repository name (expected format: project/repo)
|
||||
parts = repository.split('/')
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f'Invalid repository name format: {repository}. Expected format: project/repo'
|
||||
)
|
||||
|
||||
project_name, repo_name = parts
|
||||
|
||||
# Get repositories for the specific project
|
||||
repos_url = f'{self.base_url}/git/repositories'
|
||||
repos_params = {'api-version': '7.1-preview.1', 'project': project_name}
|
||||
|
||||
repos_data, _ = await self._make_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
raise ValueError(f'Repository not found: {repository}')
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
raise ValueError(f'Repository not found: {repository}')
|
||||
|
||||
return Repository(
|
||||
id=repo.get('id', ''),
|
||||
full_name=f'{project_name}/{repo_name}',
|
||||
git_provider=ProviderType.AZURE_DEVOPS,
|
||||
is_public=False, # Azure DevOps repos are private by default
|
||||
stargazers_count=None,
|
||||
link_header=None,
|
||||
pushed_at=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting Azure DevOps repository details: {e}')
|
||||
raise AuthenticationError(f'Failed to get repository details: {e}')
|
||||
|
||||
async def get_branches(self, repository: str) -> list[Branch]:
|
||||
"""Get branches for a repository.
|
||||
|
||||
Args:
|
||||
repository: The repository name (format: project/repo)
|
||||
|
||||
Returns:
|
||||
A list of branches
|
||||
"""
|
||||
try:
|
||||
# Parse the repository name (expected format: project/repo)
|
||||
parts = repository.split('/')
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f'Invalid repository name format: {repository}. Expected format: project/repo'
|
||||
)
|
||||
|
||||
project_name, repo_name = parts
|
||||
|
||||
# First, get the repository ID
|
||||
repo_details = await self.get_repository_details_from_repo_name(repository)
|
||||
repo_id = repo_details.id
|
||||
|
||||
# Get the branches (refs) for the repository
|
||||
refs_url = f'{self.base_url}/git/repositories/{repo_id}/refs'
|
||||
refs_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'filter': 'heads/', # Only get branch refs, not tags
|
||||
}
|
||||
|
||||
refs_data, _ = await self._make_request(refs_url, params=refs_params)
|
||||
|
||||
if not refs_data or not isinstance(refs_data, dict):
|
||||
return []
|
||||
|
||||
refs = refs_data.get('value', [])
|
||||
|
||||
# Convert to Branch objects
|
||||
result = []
|
||||
for ref in refs:
|
||||
# Extract branch name from ref name (remove 'refs/heads/' prefix)
|
||||
ref_name = ref.get('name', '')
|
||||
if ref_name.startswith('refs/heads/'):
|
||||
branch_name = ref_name[len('refs/heads/') :]
|
||||
|
||||
result.append(
|
||||
Branch(
|
||||
name=branch_name,
|
||||
commit_sha=ref.get('objectId', ''),
|
||||
protected=False, # Azure DevOps doesn't expose this information directly
|
||||
last_push_date=None, # Azure DevOps doesn't expose this information directly
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting Azure DevOps branches: {e}')
|
||||
return []
|
||||
|
||||
async def create_pr(
|
||||
self,
|
||||
repo_name: str,
|
||||
source_branch: str,
|
||||
target_branch: str,
|
||||
title: str,
|
||||
body: str | None = None,
|
||||
draft: bool = False,
|
||||
) -> str:
|
||||
"""Create a pull request in Azure DevOps.
|
||||
|
||||
Args:
|
||||
repo_name: The repository name (format: project/repo)
|
||||
source_branch: The source branch name
|
||||
target_branch: The target branch name
|
||||
title: The pull request title
|
||||
body: The pull request description (optional)
|
||||
draft: Whether the pull request should be a draft (optional)
|
||||
|
||||
Returns:
|
||||
The URL of the created pull request
|
||||
|
||||
Raises:
|
||||
ValueError: If the repository name format is invalid
|
||||
AuthenticationError: If authentication fails
|
||||
UnknownException: If the API request fails
|
||||
"""
|
||||
try:
|
||||
# Parse the repository name (expected format: project/repo)
|
||||
parts = repo_name.split('/')
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f'Invalid repository name format: {repo_name}. Expected format: project/repo'
|
||||
)
|
||||
|
||||
project_name, repo_name_only = parts
|
||||
|
||||
# Get the repository details to get the repository ID
|
||||
repo_details = await self.get_repository_details_from_repo_name(repo_name)
|
||||
repo_id = repo_details.id
|
||||
|
||||
# Prepare the pull request data
|
||||
pr_data = {
|
||||
'sourceRefName': f'refs/heads/{source_branch}',
|
||||
'targetRefName': f'refs/heads/{target_branch}',
|
||||
'title': title,
|
||||
'description': body
|
||||
or f'Pull request from {source_branch} to {target_branch}',
|
||||
'isDraft': draft,
|
||||
}
|
||||
|
||||
# Create the pull request
|
||||
pr_url = f'{self.base_url}/git/repositories/{repo_id}/pullrequests'
|
||||
pr_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
response_data, _ = await self._make_request(
|
||||
url=pr_url,
|
||||
params=pr_params,
|
||||
method=RequestMethod.POST,
|
||||
json_data=pr_data,
|
||||
)
|
||||
|
||||
if not response_data or not isinstance(response_data, dict):
|
||||
raise UnknownException(
|
||||
'Failed to create pull request: Invalid response'
|
||||
)
|
||||
|
||||
# Extract the pull request URL
|
||||
pr_id = response_data.get('pullRequestId')
|
||||
if not pr_id:
|
||||
raise UnknownException(
|
||||
'Failed to create pull request: No PR ID returned'
|
||||
)
|
||||
|
||||
# Construct the web URL for the pull request
|
||||
web_url = f'{self.organization_url}/{project_name}/_git/{repo_name_only}/pullrequest/{pr_id}'
|
||||
|
||||
logger.info(f'Successfully created Azure DevOps pull request: {web_url}')
|
||||
return web_url
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except AuthenticationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error creating Azure DevOps pull request: {e}')
|
||||
raise UnknownException(f'Failed to create pull request: {e}')
|
||||
@@ -39,7 +39,6 @@ class GitHubService(BaseGitService, GitService):
|
||||
|
||||
The class is instantiated via get_impl() in openhands.server.shared.py.
|
||||
"""
|
||||
|
||||
BASE_URL = 'https://api.github.com'
|
||||
token: SecretStr = SecretStr('')
|
||||
refresh = False
|
||||
@@ -509,6 +508,7 @@ class GitHubService(BaseGitService, GitService):
|
||||
return response['html_url']
|
||||
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
'openhands.integrations.github.github_service.GitHubService',
|
||||
|
||||
@@ -32,7 +32,6 @@ class GitLabService(BaseGitService, GitService):
|
||||
|
||||
The class is instantiated via get_impl() in openhands.server.shared.py.
|
||||
"""
|
||||
|
||||
BASE_URL = 'https://gitlab.com/api/v4'
|
||||
GRAPHQL_URL = 'https://gitlab.com/api/graphql'
|
||||
token: SecretStr = SecretStr('')
|
||||
@@ -471,6 +470,7 @@ class GitLabService(BaseGitService, GitService):
|
||||
target_branch: The name of the branch you want the changes merged into
|
||||
title: The title of the merge request (optional, defaults to a generic title)
|
||||
description: The description of the merge request (optional)
|
||||
draft: Whether to create the MR as a draft (optional, defaults to False)
|
||||
|
||||
Returns:
|
||||
- MR URL when successful
|
||||
@@ -483,7 +483,9 @@ class GitLabService(BaseGitService, GitService):
|
||||
|
||||
# Set default description if none provided
|
||||
if not description:
|
||||
description = f'Merging changes from {source_branch} into {target_branch}'
|
||||
description = (
|
||||
f'Merging changes from {source_branch} into {target_branch}'
|
||||
)
|
||||
|
||||
# Prepare the request payload
|
||||
payload = {
|
||||
@@ -498,9 +500,11 @@ class GitLabService(BaseGitService, GitService):
|
||||
url=url, params=payload, method=RequestMethod.POST
|
||||
)
|
||||
|
||||
|
||||
return response['web_url']
|
||||
|
||||
|
||||
|
||||
gitlab_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITLAB_SERVICE_CLS',
|
||||
'openhands.integrations.gitlab.gitlab_service.GitLabService',
|
||||
|
||||
@@ -14,9 +14,6 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.action import Action
|
||||
from openhands.events.action.commands import CmdRunAction
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.azure_devops.azure_devops_service import (
|
||||
AzureDevOpsServiceImpl,
|
||||
)
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.service_types import (
|
||||
@@ -30,8 +27,6 @@ from openhands.integrations.service_types import (
|
||||
)
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
AZURE_DEVOPS_AVAILABLE = True
|
||||
|
||||
|
||||
class ProviderToken(BaseModel):
|
||||
token: SecretStr | None = Field(default=None)
|
||||
@@ -113,7 +108,6 @@ class ProviderHandler:
|
||||
self.service_class_map: dict[ProviderType, type[GitService]] = {
|
||||
ProviderType.GITHUB: GithubServiceImpl,
|
||||
ProviderType.GITLAB: GitLabServiceImpl,
|
||||
ProviderType.AZURE_DEVOPS: AzureDevOpsServiceImpl,
|
||||
}
|
||||
|
||||
self.external_auth_id = external_auth_id
|
||||
@@ -130,8 +124,6 @@ class ProviderHandler:
|
||||
"""Helper method to instantiate a service for a given provider"""
|
||||
token = self.provider_tokens[provider]
|
||||
service_class = self.service_class_map[provider]
|
||||
|
||||
# All services now use base_domain consistently
|
||||
return service_class(
|
||||
user_id=token.user_id,
|
||||
external_auth_id=self.external_auth_id,
|
||||
|
||||
@@ -13,7 +13,6 @@ from openhands.server.types import AppMode
|
||||
class ProviderType(Enum):
|
||||
GITHUB = 'github'
|
||||
GITLAB = 'gitlab'
|
||||
AZURE_DEVOPS = 'azure_devops'
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
@@ -52,19 +51,6 @@ class SuggestedTask(BaseModel):
|
||||
'ciProvider': 'GitHub',
|
||||
'requestVerb': 'pull request',
|
||||
}
|
||||
elif self.git_provider == ProviderType.AZURE_DEVOPS:
|
||||
return {
|
||||
'requestType': 'Pull Request',
|
||||
'requestTypeShort': 'PR',
|
||||
'apiName': 'Azure DevOps API',
|
||||
'tokenEnvVar': 'AZURE_DEVOPS_TOKEN',
|
||||
'ciSystem': 'Azure Pipelines',
|
||||
'ciProvider': 'Azure DevOps',
|
||||
'requestVerb': 'pull request',
|
||||
'work item': 'work item',
|
||||
'repository': 'repository',
|
||||
'pull request': 'pull request',
|
||||
}
|
||||
|
||||
raise ValueError(f'Provider {self.git_provider} for suggested task prompts')
|
||||
|
||||
@@ -97,9 +83,7 @@ class SuggestedTask(BaseModel):
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: (
|
||||
int | str
|
||||
) # Support both integer IDs (GitHub/GitLab) and string UUIDs (Azure DevOps)
|
||||
id: int
|
||||
login: str
|
||||
avatar_url: str
|
||||
company: str | None = None
|
||||
@@ -115,9 +99,7 @@ class Branch(BaseModel):
|
||||
|
||||
|
||||
class Repository(BaseModel):
|
||||
id: (
|
||||
int | str
|
||||
) # Support both integer IDs (GitHub/GitLab) and string UUIDs (Azure DevOps)
|
||||
id: int
|
||||
full_name: str
|
||||
git_provider: ProviderType
|
||||
is_public: bool
|
||||
@@ -193,7 +175,7 @@ class BaseGitService(ABC):
|
||||
|
||||
|
||||
class GitService(Protocol):
|
||||
"""Protocol defining the interface for Git service providers."""
|
||||
"""Protocol defining the interface for Git service providers"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -204,15 +186,15 @@ class GitService(Protocol):
|
||||
external_token_manager: bool = False,
|
||||
base_domain: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the service with authentication details."""
|
||||
"""Initialize the service with authentication details"""
|
||||
...
|
||||
|
||||
async def get_latest_token(self) -> SecretStr | None:
|
||||
"""Get latest working token of the user."""
|
||||
"""Get latest working token of the user"""
|
||||
...
|
||||
|
||||
async def get_user(self) -> User:
|
||||
"""Get the authenticated user's information."""
|
||||
"""Get the authenticated user's information"""
|
||||
...
|
||||
|
||||
async def search_repositories(
|
||||
@@ -222,21 +204,21 @@ class GitService(Protocol):
|
||||
sort: str,
|
||||
order: str,
|
||||
) -> list[Repository]:
|
||||
"""Search for repositories."""
|
||||
"""Search for repositories"""
|
||||
...
|
||||
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
"""Get repositories for the authenticated user."""
|
||||
"""Get repositories for the authenticated user"""
|
||||
...
|
||||
|
||||
async def get_suggested_tasks(self) -> list[SuggestedTask]:
|
||||
"""Get suggested tasks for the authenticated user across all repositories."""
|
||||
"""Get suggested tasks for the authenticated user across all repositories"""
|
||||
...
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
) -> Repository:
|
||||
"""Gets all repository details from repository name."""
|
||||
"""Gets all repository details from repository name"""
|
||||
|
||||
async def get_branches(self, repository: str) -> list[Branch]:
|
||||
"""Get branches for a repository."""
|
||||
"""Get branches for a repository"""
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ issue_comment }}
|
||||
{{ issue_comment }}
|
||||
@@ -1 +1 @@
|
||||
Please fix issue number #{{ issue_number }} in your repository.
|
||||
Please fix issue number #{{ issue_number }} in your repository.
|
||||
@@ -1,9 +1,8 @@
|
||||
import traceback
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.azure_devops.azure_devops_service import (
|
||||
AzureDevOpsServiceImpl,
|
||||
)
|
||||
from openhands.integrations.github.github_service import GitHubService
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabService
|
||||
from openhands.integrations.provider import ProviderType
|
||||
@@ -13,53 +12,35 @@ async def validate_provider_token(
|
||||
token: SecretStr, base_domain: str | None = None
|
||||
) -> ProviderType | None:
|
||||
"""
|
||||
Determine whether a token is for GitHub, GitLab, or Azure DevOps by attempting to get user info
|
||||
from the services.
|
||||
Determine whether a token is for GitHub or GitLab by attempting to get user info
|
||||
from both services.
|
||||
|
||||
Args:
|
||||
token: The token to check
|
||||
base_domain: Optional base domain for the service
|
||||
|
||||
Returns:
|
||||
'github' if it's a GitHub token
|
||||
'gitlab' if it's a GitLab token
|
||||
'azure_devops' if it's an Azure DevOps token
|
||||
None if the token is invalid for all services
|
||||
None if the token is invalid for both services
|
||||
"""
|
||||
# Skip validation for empty tokens
|
||||
if token is None or not token.get_secret_value().strip():
|
||||
return None
|
||||
# Try GitHub first
|
||||
github_error = None
|
||||
try:
|
||||
github_service = GitHubService(token=token, base_domain=base_domain)
|
||||
await github_service.verify_access()
|
||||
return ProviderType.GITHUB
|
||||
except Exception as e:
|
||||
github_error = e
|
||||
logger.debug(
|
||||
f'Failed to validate Github token: {e} \n {traceback.format_exc()}'
|
||||
)
|
||||
|
||||
# Try GitLab next
|
||||
gitlab_error = None
|
||||
try:
|
||||
gitlab_service = GitLabService(token=token, base_domain=base_domain)
|
||||
await gitlab_service.get_user()
|
||||
return ProviderType.GITLAB
|
||||
except Exception as e:
|
||||
gitlab_error = e
|
||||
|
||||
# Try Azure DevOps last
|
||||
azure_devops_error = None
|
||||
try:
|
||||
azure_devops_service = AzureDevOpsServiceImpl(
|
||||
token=token, base_domain=base_domain
|
||||
logger.debug(
|
||||
f'Failed to validate GitLab token: {e} \n {traceback.format_exc()}'
|
||||
)
|
||||
await azure_devops_service.get_user()
|
||||
return ProviderType.AZURE_DEVOPS
|
||||
except Exception as e:
|
||||
azure_devops_error = e
|
||||
|
||||
logger.debug(
|
||||
f'Failed to validate token: {github_error} \n {gitlab_error} \n {azure_devops_error}'
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# OpenHands Github, Gitlab & Azure DevOps Issue Resolver 🙌
|
||||
# OpenHands Github & Gitlab Issue Resolver 🙌
|
||||
|
||||
Need help resolving issues in GitHub, GitLab, or Azure DevOps but don't have the time to do it yourself? Let an AI agent help you out!
|
||||
Need help resolving a GitHub issue but don't have the time to do it yourself? Let an AI agent help you out!
|
||||
|
||||
This tool allows you to use open-source AI agents based on [OpenHands](https://github.com/all-hands-ai/openhands)
|
||||
to attempt to resolve issues automatically. While it can handle multiple issues, it's primarily designed
|
||||
to attempt to resolve GitHub issues automatically. While it can handle multiple issues, it's primarily designed
|
||||
to help you resolve one issue at a time with high quality.
|
||||
|
||||
Getting started is simple - just follow the instructions below.
|
||||
@@ -74,8 +74,8 @@ If you prefer to run the resolver programmatically instead of using GitHub Actio
|
||||
pip install openhands-ai
|
||||
```
|
||||
|
||||
2. Create an access token for your platform:
|
||||
- Create a GitHub access token
|
||||
2. Create a GitHub or GitLab access token:
|
||||
- Create a GitHub acces token
|
||||
- Visit [GitHub's token settings](https://github.com/settings/personal-access-tokens/new)
|
||||
- Create a fine-grained token with these scopes:
|
||||
- "Content"
|
||||
@@ -84,7 +84,7 @@ pip install openhands-ai
|
||||
- "Workflows"
|
||||
- If you don't have push access to the target repo, you can fork it first
|
||||
|
||||
- Create a GitLab access token
|
||||
- Create a GitLab acces token
|
||||
- Visit [GitLab's token settings](https://gitlab.com/-/user_settings/personal_access_tokens)
|
||||
- Create a fine-grained token with these scopes:
|
||||
- 'api'
|
||||
@@ -93,30 +93,20 @@ pip install openhands-ai
|
||||
- 'read_repository'
|
||||
- 'write_repository'
|
||||
|
||||
- Create an Azure DevOps access token
|
||||
- Visit [Azure DevOps Personal Access Tokens](https://dev.azure.com/your-organization/_usersSettings/tokens)
|
||||
- Create a token with these scopes:
|
||||
- "Code (Read & Write)"
|
||||
- "Work Items (Read & Write)"
|
||||
- "Pull Request Threads (Read & Write)"
|
||||
- "Pull Request Contribute"
|
||||
|
||||
3. Set up environment variables:
|
||||
|
||||
```bash
|
||||
|
||||
# GitHub credentials
|
||||
|
||||
export GITHUB_TOKEN="your-github-token"
|
||||
export GIT_USERNAME="your-github-username" # Optional, defaults to token owner
|
||||
|
||||
# GitLab credentials if you're using GitLab repo
|
||||
|
||||
export GITLAB_TOKEN="your-gitlab-token"
|
||||
export GIT_USERNAME="your-gitlab-username" # Optional, defaults to token owner
|
||||
|
||||
# Azure DevOps credentials if you're using Azure DevOps repo
|
||||
export AZURE_DEVOPS_TOKEN="your-azure-devops-token"
|
||||
export GIT_USERNAME="your-azure-devops-username" # Optional, defaults to token owner
|
||||
|
||||
# LLM configuration
|
||||
|
||||
export LLM_MODEL="anthropic/claude-sonnet-4-20250514" # Recommended
|
||||
|
||||
@@ -1,915 +0,0 @@
|
||||
import asyncio
|
||||
import base64
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import RequestMethod
|
||||
from openhands.resolver.interfaces.issue import (
|
||||
Issue,
|
||||
IssueHandlerInterface,
|
||||
ReviewThread,
|
||||
)
|
||||
|
||||
|
||||
class AzureDevOpsIssueHandler(IssueHandlerInterface):
|
||||
def __init__(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
token: str,
|
||||
username: str | None = None,
|
||||
base_domain: str = 'dev.azure.com',
|
||||
):
|
||||
"""Initialize an Azure DevOps issue handler.
|
||||
|
||||
Args:
|
||||
owner: The owner (organization) of the repository
|
||||
repo: The name of the repository (format: project/repo)
|
||||
token: The Azure DevOps personal access token
|
||||
username: Optional Azure DevOps username
|
||||
base_domain: The domain for Azure DevOps (default: "dev.azure.com")
|
||||
"""
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
self.token = token
|
||||
self.username = username
|
||||
self.base_domain = base_domain
|
||||
|
||||
# Parse the repository name (expected format: project/repo)
|
||||
parts = repo.split('/')
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f'Invalid repository name format: {repo}. Expected format: project/repo'
|
||||
)
|
||||
|
||||
self.project_name, self.repo_name = parts
|
||||
|
||||
self.base_url = self.get_base_url()
|
||||
self.download_url = self.get_download_url()
|
||||
self.clone_url = self.get_clone_url()
|
||||
self.headers = self.get_headers()
|
||||
|
||||
# Set up API base URL
|
||||
self.api_base_url = f'https://{self.base_domain}/{self.owner}/_apis'
|
||||
|
||||
def set_owner(self, owner: str) -> None:
|
||||
self.owner = owner
|
||||
|
||||
def get_headers(self) -> dict[str, str]:
|
||||
# Azure DevOps uses Basic authentication with PAT
|
||||
# Username can be empty, password is the PAT
|
||||
credentials = base64.b64encode(f':{self.token}'.encode()).decode()
|
||||
return {
|
||||
'Authorization': f'Basic {credentials}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
async def _make_api_request(
|
||||
self,
|
||||
url: str,
|
||||
method: RequestMethod = RequestMethod.GET,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
) -> dict | list | None:
|
||||
"""Make an HTTP request to the Azure DevOps API."""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
if method == RequestMethod.GET:
|
||||
response = await client.get(
|
||||
url, headers=self.headers, params=params
|
||||
)
|
||||
elif method == RequestMethod.POST:
|
||||
response = await client.post(
|
||||
url, headers=self.headers, params=params, json=json_data
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported HTTP method: {method}')
|
||||
|
||||
if response.status_code >= 400:
|
||||
logger.error(
|
||||
f'Azure DevOps API error: {response.status_code} - {response.text}'
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except Exception:
|
||||
return response.text
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Request error: {e}')
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f'Unexpected error: {e}')
|
||||
return None
|
||||
|
||||
def get_base_url(self) -> str:
|
||||
return f'https://{self.base_domain}/{self.owner}/{self.project_name}/_apis/git/repositories/{self.repo_name}'
|
||||
|
||||
def get_authorize_url(self) -> str:
|
||||
return f'https://{self.username}:{self.token}@{self.base_domain}/'
|
||||
|
||||
def get_branch_url(self, branch_name: str) -> str:
|
||||
return self.get_base_url() + f'/refs?filter=heads/{branch_name}'
|
||||
|
||||
def get_download_url(self) -> str:
|
||||
return f'https://{self.base_domain}/{self.owner}/{self.project_name}/_apis/wit/workitems'
|
||||
|
||||
def get_clone_url(self) -> str:
|
||||
return f'https://{self.username}:{self.token}@{self.base_domain}/{self.owner}/{self.project_name}/_git/{self.repo_name}'
|
||||
|
||||
def get_graphql_url(self) -> str:
|
||||
return f'https://{self.base_domain}/{self.owner}/_apis/graphql'
|
||||
|
||||
def get_compare_url(self, branch_name: str) -> str:
|
||||
return f'https://{self.base_domain}/{self.owner}/{self.project_name}/_git/{self.repo_name}/branchCompare?baseVersion=GC{self.get_default_branch_name()}&targetVersion=GC{branch_name}'
|
||||
|
||||
def get_converted_issues(
|
||||
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
|
||||
) -> list[Issue]:
|
||||
"""Download issues from Azure DevOps.
|
||||
|
||||
Args:
|
||||
issue_numbers: The numbers of the issues to download
|
||||
comment_id: The ID of a single comment, if provided, otherwise all comments
|
||||
|
||||
Returns:
|
||||
List of Azure DevOps issues.
|
||||
"""
|
||||
if not issue_numbers:
|
||||
raise ValueError('Unspecified issue number')
|
||||
|
||||
all_issues = self.download_issues()
|
||||
logger.info(f'Limiting resolving to issues {issue_numbers}.')
|
||||
all_issues = [issue for issue in all_issues if issue['id'] in issue_numbers]
|
||||
|
||||
if len(issue_numbers) == 1 and not all_issues:
|
||||
raise ValueError(f'Issue {issue_numbers[0]} not found')
|
||||
|
||||
converted_issues = []
|
||||
for issue in all_issues:
|
||||
# Check for required fields (id and title)
|
||||
if any(
|
||||
[
|
||||
issue.get('fields', {}).get(key) is None
|
||||
for key in ['System.Id', 'System.Title']
|
||||
]
|
||||
):
|
||||
logger.warning(f'Skipping issue {issue} as it is missing id or title.')
|
||||
continue
|
||||
|
||||
# Handle empty body by using empty string
|
||||
description = issue.get('fields', {}).get('System.Description', '')
|
||||
if description is None:
|
||||
description = ''
|
||||
|
||||
# Get issue thread comments
|
||||
thread_comments = self.get_issue_comments(
|
||||
issue['id'], comment_id=comment_id
|
||||
)
|
||||
|
||||
# Convert empty lists to None for optional fields
|
||||
issue_details = Issue(
|
||||
owner=self.owner,
|
||||
repo=self.repo,
|
||||
number=issue['id'],
|
||||
title=issue['fields']['System.Title'],
|
||||
body=description,
|
||||
thread_comments=thread_comments,
|
||||
review_comments=None, # Initialize review comments as None for regular issues
|
||||
)
|
||||
|
||||
converted_issues.append(issue_details)
|
||||
|
||||
return converted_issues
|
||||
|
||||
def download_issues(self) -> list[Any]:
|
||||
"""Download issues from Azure DevOps using HTTP API calls."""
|
||||
return asyncio.run(self._download_issues_async())
|
||||
|
||||
async def _download_issues_async(self) -> list[Any]:
|
||||
"""Download issues from Azure DevOps asynchronously."""
|
||||
# Use WIQL to query for open bugs
|
||||
wiql_url = f'{self.api_base_url}/wit/wiql'
|
||||
wiql_params = {'api-version': '7.1-preview.2'}
|
||||
|
||||
wiql_query = {
|
||||
'query': f"""
|
||||
select [System.Id],
|
||||
[System.WorkItemType],
|
||||
[System.Title],
|
||||
[System.State],
|
||||
[System.Description]
|
||||
from WorkItems
|
||||
where [System.TeamProject] = '{self.project_name}'
|
||||
and [System.WorkItemType] in ('Bug', 'Issue', 'Task')
|
||||
and [System.State] <> 'Closed'
|
||||
and [System.State] <> 'Resolved'
|
||||
and [System.State] <> 'Done'
|
||||
order by [System.ChangedDate] desc
|
||||
"""
|
||||
}
|
||||
|
||||
wiql_data = await self._make_api_request(
|
||||
wiql_url,
|
||||
method=RequestMethod.POST,
|
||||
params=wiql_params,
|
||||
json_data=wiql_query,
|
||||
)
|
||||
|
||||
if not wiql_data or not isinstance(wiql_data, dict):
|
||||
return []
|
||||
|
||||
work_items = wiql_data.get('workItems', [])
|
||||
|
||||
# Get full work item details
|
||||
all_issues = []
|
||||
for work_item in work_items:
|
||||
work_item_id = work_item.get('id')
|
||||
if not work_item_id:
|
||||
continue
|
||||
|
||||
# Get work item details
|
||||
work_item_url = f'{self.api_base_url}/wit/workitems/{work_item_id}'
|
||||
work_item_params = {'api-version': '7.1-preview.3'}
|
||||
|
||||
work_item_data = await self._make_api_request(
|
||||
work_item_url, params=work_item_params
|
||||
)
|
||||
|
||||
if work_item_data and isinstance(work_item_data, dict):
|
||||
# Convert the work item to a dictionary format similar to GitHub/GitLab
|
||||
issue = {
|
||||
'id': work_item_data.get('id'),
|
||||
'fields': work_item_data.get('fields', {}),
|
||||
}
|
||||
all_issues.append(issue)
|
||||
|
||||
return all_issues
|
||||
|
||||
def get_issue_comments(
|
||||
self, issue_number: int, comment_id: int | None = None
|
||||
) -> list[str] | None:
|
||||
"""Download comments for a specific issue from Azure DevOps."""
|
||||
return asyncio.run(self._get_issue_comments_async(issue_number, comment_id))
|
||||
|
||||
async def _get_issue_comments_async(
|
||||
self, issue_number: int, comment_id: int | None = None
|
||||
) -> list[str] | None:
|
||||
"""Download comments for a specific issue from Azure DevOps asynchronously."""
|
||||
# Get the comments for the work item
|
||||
comments_url = f'{self.api_base_url}/wit/workItems/{issue_number}/comments'
|
||||
comments_params = {'api-version': '7.1-preview.3'}
|
||||
|
||||
comments_data = await self._make_api_request(
|
||||
comments_url, params=comments_params
|
||||
)
|
||||
|
||||
if not comments_data or not isinstance(comments_data, dict):
|
||||
return None
|
||||
|
||||
comments = comments_data.get('comments', [])
|
||||
|
||||
all_comments = []
|
||||
if comments:
|
||||
if comment_id:
|
||||
matching_comment = next(
|
||||
(
|
||||
comment.get('text', '')
|
||||
for comment in comments
|
||||
if comment.get('id') == comment_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching_comment:
|
||||
return [matching_comment]
|
||||
else:
|
||||
all_comments = [
|
||||
comment.get('text', '')
|
||||
for comment in comments
|
||||
if comment.get('text')
|
||||
]
|
||||
|
||||
return all_comments if all_comments else None
|
||||
|
||||
def branch_exists(self, branch_name: str) -> bool:
|
||||
"""Check if a branch exists."""
|
||||
return asyncio.run(self._branch_exists_async(branch_name))
|
||||
|
||||
async def _branch_exists_async(self, branch_name: str) -> bool:
|
||||
"""Check if a branch exists asynchronously."""
|
||||
logger.info(f'Checking if branch {branch_name} exists...')
|
||||
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return False
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return False
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Get the branches (refs) for the repository
|
||||
refs_url = f'{self.api_base_url}/git/repositories/{repo_id}/refs'
|
||||
refs_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'filter': f'heads/{branch_name}',
|
||||
}
|
||||
|
||||
refs_data = await self._make_api_request(refs_url, params=refs_params)
|
||||
|
||||
if not refs_data or not isinstance(refs_data, dict):
|
||||
return False
|
||||
|
||||
refs = refs_data.get('value', [])
|
||||
exists = len(refs) > 0
|
||||
|
||||
logger.info(f'Branch {branch_name} exists: {exists}')
|
||||
return exists
|
||||
except Exception as e:
|
||||
logger.warning(f'Error checking if branch exists: {e}')
|
||||
return False
|
||||
|
||||
def get_branch_name(self, base_branch_name: str) -> str:
|
||||
branch_name = base_branch_name
|
||||
attempt = 1
|
||||
while self.branch_exists(branch_name):
|
||||
attempt += 1
|
||||
branch_name = f'{base_branch_name}-try{attempt}'
|
||||
return branch_name
|
||||
|
||||
def reply_to_comment(self, pr_number: int, comment_id: str, reply: str) -> None:
|
||||
"""Reply to a comment on a pull request."""
|
||||
asyncio.run(self._reply_to_comment_async(pr_number, comment_id, reply))
|
||||
|
||||
async def _reply_to_comment_async(
|
||||
self, pr_number: int, comment_id: str, reply: str
|
||||
) -> None:
|
||||
"""Reply to a comment on a pull request asynchronously."""
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Create a comment reply
|
||||
comment_reply = f'Openhands fix success summary\n\n\n{reply}'
|
||||
|
||||
# Add the comment to the thread
|
||||
comment_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullRequests/{pr_number}/threads/{comment_id}/comments'
|
||||
comment_params = {'api-version': '7.1-preview.1'}
|
||||
comment_data = {'content': comment_reply}
|
||||
|
||||
await self._make_api_request(
|
||||
comment_url,
|
||||
method=RequestMethod.POST,
|
||||
params=comment_params,
|
||||
json_data=comment_data,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Error replying to comment: {e}')
|
||||
|
||||
def get_pull_url(self, pr_number: int) -> str:
|
||||
return f'https://{self.base_domain}/{self.owner}/{self.project_name}/_git/{self.repo_name}/pullrequest/{pr_number}'
|
||||
|
||||
def get_default_branch_name(self) -> str:
|
||||
"""Get the default branch name."""
|
||||
return asyncio.run(self._get_default_branch_name_async())
|
||||
|
||||
async def _get_default_branch_name_async(self) -> str:
|
||||
"""Get the default branch name asynchronously."""
|
||||
try:
|
||||
# First, get the repository
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return 'main' # Default to 'main' if repository not found
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return 'main' # Default to 'main' if repository not found
|
||||
|
||||
# Get the default branch
|
||||
default_branch = repo.get('defaultBranch', 'refs/heads/main')
|
||||
return default_branch.replace('refs/heads/', '')
|
||||
except Exception as e:
|
||||
logger.warning(f'Error getting default branch: {e}')
|
||||
return 'main' # Default to 'main' if an error occurs
|
||||
|
||||
def create_pull_request(self, data: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Create a pull request."""
|
||||
return asyncio.run(self._create_pull_request_async(data))
|
||||
|
||||
async def _create_pull_request_async(
|
||||
self, data: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Create a pull request asynchronously."""
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
raise RuntimeError(f'Repository not found: {self.repo_name}')
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
raise RuntimeError(f'Repository not found: {self.repo_name}')
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Create the pull request
|
||||
pr_data = {
|
||||
'sourceRefName': f'refs/heads/{data.get("head", "")}',
|
||||
'targetRefName': f'refs/heads/{data.get("base", "")}',
|
||||
'title': data.get('title', ''),
|
||||
'description': data.get('body', ''),
|
||||
}
|
||||
|
||||
pr_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullrequests'
|
||||
pr_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
created_pr = await self._make_api_request(
|
||||
pr_url, method=RequestMethod.POST, params=pr_params, json_data=pr_data
|
||||
)
|
||||
|
||||
if not created_pr or not isinstance(created_pr, dict):
|
||||
raise RuntimeError('Failed to create pull request')
|
||||
|
||||
# Convert to a format similar to GitHub/GitLab
|
||||
pr_id = created_pr.get('pullRequestId')
|
||||
if pr_id is None:
|
||||
raise RuntimeError('Pull request ID not found in response')
|
||||
|
||||
pr_result = {
|
||||
'id': pr_id,
|
||||
'number': pr_id,
|
||||
'html_url': self.get_pull_url(pr_id),
|
||||
}
|
||||
|
||||
return pr_result
|
||||
except Exception as e:
|
||||
if '403' in str(e):
|
||||
raise RuntimeError(
|
||||
'Failed to create pull request due to missing permissions. '
|
||||
'Make sure that the provided token has push permissions for the repository.'
|
||||
)
|
||||
raise RuntimeError(f'Failed to create pull request: {e}')
|
||||
|
||||
def request_reviewers(self, reviewer: str, pr_number: int) -> None:
|
||||
"""Request reviewers for a pull request."""
|
||||
asyncio.run(self._request_reviewers_async(reviewer, pr_number))
|
||||
|
||||
async def _request_reviewers_async(self, reviewer: str, pr_number: int) -> None:
|
||||
"""Request reviewers for a pull request asynchronously."""
|
||||
# Azure DevOps doesn't have a direct API for requesting reviewers
|
||||
# Instead, we'll add a comment mentioning the reviewer
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Create a comment mentioning the reviewer
|
||||
comment = f'@{reviewer} Please review this pull request.'
|
||||
|
||||
# Add the comment to the pull request
|
||||
thread_data = {
|
||||
'comments': [{'content': comment}],
|
||||
'status': 'active',
|
||||
}
|
||||
|
||||
thread_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullRequests/{pr_number}/threads'
|
||||
thread_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
await self._make_api_request(
|
||||
thread_url,
|
||||
method=RequestMethod.POST,
|
||||
params=thread_params,
|
||||
json_data=thread_data,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to request review from {reviewer}: {e}')
|
||||
|
||||
def send_comment_msg(self, issue_number: int, msg: str) -> None:
|
||||
"""Send a comment message to an Azure DevOps issue or pull request."""
|
||||
asyncio.run(self._send_comment_msg_async(issue_number, msg))
|
||||
|
||||
async def _send_comment_msg_async(self, issue_number: int, msg: str) -> None:
|
||||
"""Send a comment message to an Azure DevOps issue or pull request asynchronously."""
|
||||
try:
|
||||
# Add the comment to the work item
|
||||
comment_url = f'{self.api_base_url}/wit/workItems/{issue_number}/comments'
|
||||
comment_params = {'api-version': '7.1-preview.3'}
|
||||
comment_data = {'text': msg}
|
||||
|
||||
await self._make_api_request(
|
||||
comment_url,
|
||||
method=RequestMethod.POST,
|
||||
params=comment_params,
|
||||
json_data=comment_data,
|
||||
)
|
||||
logger.info(f'Comment added to the issue: {msg}')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to post comment: {e}')
|
||||
|
||||
def get_context_from_external_issues_references(
|
||||
self,
|
||||
closing_issues: list[str],
|
||||
closing_issue_numbers: list[int],
|
||||
issue_body: str,
|
||||
review_comments: list[str] | None,
|
||||
review_threads: list[ReviewThread],
|
||||
thread_comments: list[str] | None,
|
||||
) -> list[str]:
|
||||
"""Get context from external issue references."""
|
||||
# This method can remain largely the same as it doesn't use Azure DevOps SDK
|
||||
context_items = []
|
||||
if closing_issues:
|
||||
context_items.append(f'Closing issues: {", ".join(closing_issues)}')
|
||||
if closing_issue_numbers:
|
||||
context_items.append(
|
||||
f'Closing issue numbers: {", ".join(map(str, closing_issue_numbers))}'
|
||||
)
|
||||
if issue_body:
|
||||
context_items.append(f'Issue body: {issue_body}')
|
||||
if review_comments:
|
||||
context_items.extend(review_comments)
|
||||
if review_threads:
|
||||
for thread in review_threads:
|
||||
context_items.append(f'Review thread: {thread.comment}')
|
||||
if thread_comments:
|
||||
context_items.extend(thread_comments)
|
||||
return context_items
|
||||
|
||||
|
||||
class AzureDevOpsPRHandler(AzureDevOpsIssueHandler):
|
||||
"""Azure DevOps Pull Request handler that extends the issue handler."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
token: str,
|
||||
username: str | None = None,
|
||||
base_domain: str = 'dev.azure.com',
|
||||
):
|
||||
"""Initialize an Azure DevOps PR handler.
|
||||
|
||||
Args:
|
||||
owner: The owner (organization) of the repository
|
||||
repo: The name of the repository (format: project/repo)
|
||||
token: The Azure DevOps personal access token
|
||||
username: Optional Azure DevOps username
|
||||
base_domain: The domain for Azure DevOps (default: "dev.azure.com")
|
||||
"""
|
||||
super().__init__(owner, repo, token, username, base_domain)
|
||||
|
||||
def download_issues(self) -> list[Any]:
|
||||
"""Download pull requests from Azure DevOps."""
|
||||
return asyncio.run(self._download_pull_requests_async())
|
||||
|
||||
async def _download_pull_requests_async(self) -> list[Any]:
|
||||
"""Download pull requests from Azure DevOps asynchronously."""
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return []
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return []
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Get all active pull requests for the repository
|
||||
prs_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullrequests'
|
||||
prs_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'searchCriteria.status': 'active',
|
||||
}
|
||||
|
||||
prs_data = await self._make_api_request(prs_url, params=prs_params)
|
||||
|
||||
if not prs_data or not isinstance(prs_data, dict):
|
||||
return []
|
||||
|
||||
pull_requests = prs_data.get('value', [])
|
||||
|
||||
# Convert pull requests to the issue format
|
||||
all_issues = []
|
||||
for pr in pull_requests:
|
||||
# Convert the PR to a dictionary format similar to issues
|
||||
issue = {
|
||||
'id': pr.get('pullRequestId'),
|
||||
'fields': {
|
||||
'System.Id': pr.get('pullRequestId'),
|
||||
'System.Title': pr.get('title', ''),
|
||||
'System.Description': pr.get('description', ''),
|
||||
},
|
||||
'source_branch': pr.get('sourceRefName', ''),
|
||||
'repository': repo,
|
||||
}
|
||||
all_issues.append(issue)
|
||||
|
||||
return all_issues
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Error downloading pull requests: {e}')
|
||||
return []
|
||||
|
||||
def get_converted_issues(
|
||||
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
|
||||
) -> list[Issue]:
|
||||
"""Download pull requests from Azure DevOps.
|
||||
|
||||
Args:
|
||||
issue_numbers: The numbers of the pull requests to download
|
||||
comment_id: The ID of a single comment, if provided, otherwise all comments
|
||||
|
||||
Returns:
|
||||
List of Azure DevOps pull requests as Issue objects.
|
||||
"""
|
||||
if not issue_numbers:
|
||||
raise ValueError('Unspecified issue number')
|
||||
|
||||
all_issues = self.download_issues()
|
||||
logger.info(f'Limiting resolving to issues {issue_numbers}.')
|
||||
all_issues = [issue for issue in all_issues if issue['id'] in issue_numbers]
|
||||
|
||||
if len(issue_numbers) == 1 and not all_issues:
|
||||
raise ValueError(f'Issue {issue_numbers[0]} not found')
|
||||
|
||||
converted_issues = []
|
||||
for issue in all_issues:
|
||||
# Get PR metadata
|
||||
(
|
||||
closing_issues,
|
||||
closing_issue_numbers,
|
||||
review_bodies,
|
||||
review_threads,
|
||||
thread_ids,
|
||||
) = self.download_pr_metadata(issue['id'], comment_id)
|
||||
|
||||
# Create the Issue object
|
||||
converted_issue = Issue(
|
||||
number=issue['id'],
|
||||
title=issue['fields']['System.Title'],
|
||||
body=issue['fields']['System.Description'],
|
||||
owner=self.owner,
|
||||
repo=f'{self.project_name}/{self.repo_name}',
|
||||
head_branch=issue['source_branch'].replace('refs/heads/', ''),
|
||||
closing_issues=closing_issues,
|
||||
closing_issue_numbers=closing_issue_numbers,
|
||||
review_bodies=review_bodies,
|
||||
review_threads=review_threads,
|
||||
thread_ids=thread_ids,
|
||||
)
|
||||
converted_issues.append(converted_issue)
|
||||
|
||||
return converted_issues
|
||||
|
||||
def download_pr_metadata(
|
||||
self, pull_number: int, comment_id: int | None = None
|
||||
) -> tuple[list[str], list[int], list[str] | None, list[ReviewThread], list[str]]:
|
||||
"""Get metadata for a pull request."""
|
||||
return asyncio.run(self._download_pr_metadata_async(pull_number, comment_id))
|
||||
|
||||
async def _download_pr_metadata_async(
|
||||
self, pull_number: int, comment_id: int | None = None
|
||||
) -> tuple[list[str], list[int], list[str] | None, list[ReviewThread], list[str]]:
|
||||
"""Get metadata for a pull request asynchronously.
|
||||
|
||||
Args:
|
||||
pull_number: The number of the pull request to query.
|
||||
comment_id: Optional ID of a specific comment to focus on.
|
||||
|
||||
Returns:
|
||||
Tuple containing:
|
||||
1. List of closing issue bodies
|
||||
2. List of closing issue numbers
|
||||
3. List of review bodies
|
||||
4. List of review threads
|
||||
5. List of thread IDs
|
||||
"""
|
||||
try:
|
||||
# First, get the repository ID
|
||||
repos_url = f'{self.api_base_url}/git/repositories'
|
||||
repos_params = {
|
||||
'api-version': '7.1-preview.1',
|
||||
'project': self.project_name,
|
||||
}
|
||||
|
||||
repos_data = await self._make_api_request(repos_url, params=repos_params)
|
||||
|
||||
if not repos_data or not isinstance(repos_data, dict):
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return [], [], None, [], []
|
||||
|
||||
repositories = repos_data.get('value', [])
|
||||
repo = next(
|
||||
(
|
||||
r
|
||||
for r in repositories
|
||||
if r.get('name', '').lower() == self.repo_name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not repo:
|
||||
logger.warning(f'Repository not found: {self.repo_name}')
|
||||
return [], [], None, [], []
|
||||
|
||||
repo_id = repo.get('id')
|
||||
|
||||
# Get the pull request details
|
||||
pr_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullRequests/{pull_number}'
|
||||
pr_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
pr_data = await self._make_api_request(pr_url, params=pr_params)
|
||||
|
||||
if not pr_data:
|
||||
logger.warning(f'Pull request {pull_number} not found')
|
||||
return [], [], None, [], []
|
||||
|
||||
# Get threads (comments) for the pull request
|
||||
threads_url = f'{self.api_base_url}/git/repositories/{repo_id}/pullRequests/{pull_number}/threads'
|
||||
threads_params = {'api-version': '7.1-preview.1'}
|
||||
|
||||
threads_data = await self._make_api_request(
|
||||
threads_url, params=threads_params
|
||||
)
|
||||
|
||||
review_threads = []
|
||||
thread_ids = []
|
||||
review_bodies = []
|
||||
|
||||
if threads_data and isinstance(threads_data, dict):
|
||||
threads = threads_data.get('value', [])
|
||||
|
||||
for thread in threads:
|
||||
thread_id = str(thread.get('id', ''))
|
||||
thread_ids.append(thread_id)
|
||||
|
||||
comments = thread.get('comments', [])
|
||||
if comments:
|
||||
# Get the first comment as the main review body
|
||||
first_comment = comments[0]
|
||||
content = first_comment.get('content', '')
|
||||
if content:
|
||||
review_bodies.append(content)
|
||||
|
||||
# Create review thread
|
||||
review_thread = ReviewThread(
|
||||
id=thread_id,
|
||||
body=content,
|
||||
line=None, # Azure DevOps doesn't provide line numbers in the same way
|
||||
start_line=None,
|
||||
original_line=None,
|
||||
original_start_line=None,
|
||||
diff_hunk='', # Would need additional API call to get diff
|
||||
path='', # Would need additional API call to get file path
|
||||
)
|
||||
review_threads.append(review_thread)
|
||||
|
||||
# For now, we don't extract closing issues from PR description
|
||||
# This would require parsing the description text
|
||||
closing_issues: list[str] = []
|
||||
closing_issue_numbers: list[int] = []
|
||||
|
||||
return (
|
||||
closing_issues,
|
||||
closing_issue_numbers,
|
||||
review_bodies if review_bodies else None,
|
||||
review_threads,
|
||||
thread_ids,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Error downloading PR metadata: {e}')
|
||||
return [], [], None, [], []
|
||||
@@ -121,5 +121,5 @@ class IssueHandlerInterface(ABC):
|
||||
def get_converted_issues(
|
||||
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
|
||||
) -> list[Issue]:
|
||||
"""Download issues from the git provider (GitHub, GitLab, or Azure DevOps)."""
|
||||
"""Download issues from Gitlab."""
|
||||
pass
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.resolver.interfaces.azure_devops import (
|
||||
AzureDevOpsIssueHandler,
|
||||
AzureDevOpsPRHandler,
|
||||
)
|
||||
from openhands.resolver.interfaces.github import GithubIssueHandler, GithubPRHandler
|
||||
from openhands.resolver.interfaces.gitlab import GitlabIssueHandler, GitlabPRHandler
|
||||
from openhands.resolver.interfaces.issue_definitions import (
|
||||
@@ -46,7 +42,7 @@ class IssueHandlerFactory:
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
elif self.platform == ProviderType.GITLAB:
|
||||
else: # platform == Platform.GITLAB
|
||||
return ServiceContextIssue(
|
||||
GitlabIssueHandler(
|
||||
self.owner,
|
||||
@@ -57,19 +53,6 @@ class IssueHandlerFactory:
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
elif self.platform == ProviderType.AZURE_DEVOPS:
|
||||
return ServiceContextIssue(
|
||||
AzureDevOpsIssueHandler(
|
||||
self.owner,
|
||||
self.repo,
|
||||
self.token,
|
||||
self.username,
|
||||
self.base_domain,
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported platform: {self.platform}')
|
||||
elif self.issue_type == 'pr':
|
||||
if self.platform == ProviderType.GITHUB:
|
||||
return ServiceContextPR(
|
||||
@@ -82,7 +65,7 @@ class IssueHandlerFactory:
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
elif self.platform == ProviderType.GITLAB:
|
||||
else: # platform == Platform.GITLAB
|
||||
return ServiceContextPR(
|
||||
GitlabPRHandler(
|
||||
self.owner,
|
||||
@@ -93,18 +76,5 @@ class IssueHandlerFactory:
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
elif self.platform == ProviderType.AZURE_DEVOPS:
|
||||
return ServiceContextPR(
|
||||
AzureDevOpsPRHandler(
|
||||
self.owner,
|
||||
self.repo,
|
||||
self.token,
|
||||
self.username,
|
||||
self.base_domain,
|
||||
),
|
||||
self.llm_config,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported platform: {self.platform}')
|
||||
else:
|
||||
raise ValueError(f'Invalid issue type: {self.issue_type}')
|
||||
|
||||
@@ -50,7 +50,6 @@ AGENT_CLASS = 'CodeActAgent'
|
||||
|
||||
class IssueResolver:
|
||||
GITLAB_CI = os.getenv('GITLAB_CI') == 'true'
|
||||
AZURE_DEVOPS_CI = os.getenv('TF_BUILD') == 'True'
|
||||
|
||||
def __init__(self, args: Namespace) -> None:
|
||||
"""Initialize the IssueResolver with the given parameters.
|
||||
@@ -77,12 +76,7 @@ class IssueResolver:
|
||||
raise ValueError('Invalid repository format. Expected owner/repo')
|
||||
owner, repo = parts
|
||||
|
||||
token = (
|
||||
args.token
|
||||
or os.getenv('GITHUB_TOKEN')
|
||||
or os.getenv('GITLAB_TOKEN')
|
||||
or os.getenv('AZURE_DEVOPS_TOKEN')
|
||||
)
|
||||
token = args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
|
||||
username = args.username if args.username else os.getenv('GIT_USERNAME')
|
||||
if not username:
|
||||
raise ValueError('Username is required.')
|
||||
@@ -126,11 +120,7 @@ class IssueResolver:
|
||||
base_domain = args.base_domain
|
||||
if base_domain is None:
|
||||
base_domain = (
|
||||
'github.com'
|
||||
if platform == ProviderType.GITHUB
|
||||
else 'gitlab.com'
|
||||
if platform == ProviderType.GITLAB
|
||||
else 'dev.azure.com'
|
||||
'github.com' if platform == ProviderType.GITHUB else 'gitlab.com'
|
||||
)
|
||||
|
||||
self.output_dir = args.output_dir
|
||||
@@ -250,14 +240,6 @@ class IssueResolver:
|
||||
if user_id == 0:
|
||||
sandbox_config.user_id = get_unique_uid()
|
||||
|
||||
# Configure sandbox for Azure DevOps CI environment
|
||||
if cls.AZURE_DEVOPS_CI:
|
||||
sandbox_config.use_host_network = False
|
||||
sandbox_config.enable_auto_lint = True
|
||||
sandbox_config.runtime_startup_env_vars = {
|
||||
'TF_BUILD': 'True',
|
||||
}
|
||||
|
||||
openhands_config.sandbox.base_container_image = (
|
||||
sandbox_config.base_container_image
|
||||
)
|
||||
@@ -291,9 +273,7 @@ class IssueResolver:
|
||||
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
|
||||
raise RuntimeError(f'Failed to change directory to /workspace.\n{obs}')
|
||||
|
||||
if (self.platform == ProviderType.GITLAB and self.GITLAB_CI) or (
|
||||
self.platform == ProviderType.AZURE_DEVOPS and self.AZURE_DEVOPS_CI
|
||||
):
|
||||
if self.platform == ProviderType.GITLAB and self.GITLAB_CI:
|
||||
action = CmdRunAction(command='sudo chown -R 1001:0 /workspace/*')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
@@ -355,9 +335,7 @@ class IssueResolver:
|
||||
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
|
||||
raise RuntimeError(f'Failed to set git config. Observation: {obs}')
|
||||
|
||||
if (self.platform == ProviderType.GITLAB and self.GITLAB_CI) or (
|
||||
self.platform == ProviderType.AZURE_DEVOPS and self.AZURE_DEVOPS_CI
|
||||
):
|
||||
if self.platform == ProviderType.GITLAB and self.GITLAB_CI:
|
||||
action = CmdRunAction(command='sudo git add -A')
|
||||
else:
|
||||
action = CmdRunAction(command='git add -A')
|
||||
|
||||
@@ -116,7 +116,7 @@ def main() -> None:
|
||||
'--base-domain',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Base domain for the git server (defaults to "github.com" for GitHub, "gitlab.com" for GitLab, and "dev.azure.com" for Azure DevOps)',
|
||||
help='Base domain for the git server (defaults to "github.com" for GitHub and "gitlab.com" for GitLab)',
|
||||
)
|
||||
|
||||
my_args = parser.parse_args()
|
||||
|
||||
@@ -11,7 +11,6 @@ from openhands.core.config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.resolver.interfaces.azure_devops import AzureDevOpsIssueHandler
|
||||
from openhands.resolver.interfaces.github import GithubIssueHandler
|
||||
from openhands.resolver.interfaces.gitlab import GitlabIssueHandler
|
||||
from openhands.resolver.interfaces.issue import Issue
|
||||
@@ -236,55 +235,40 @@ def send_pull_request(
|
||||
pr_title: str | None = None,
|
||||
base_domain: str | None = None,
|
||||
) -> str:
|
||||
"""Send a pull request to a GitHub, GitLab, or Azure DevOps repository.
|
||||
"""Send a pull request to a GitHub or Gitlab repository.
|
||||
|
||||
Args:
|
||||
issue: The issue to send the pull request for
|
||||
token: The token to use for authentication
|
||||
username: The username, if provided
|
||||
token: The GitHub or Gitlab token to use for authentication
|
||||
username: The GitHub or Gitlab username, if provided
|
||||
platform: The platform of the repository.
|
||||
patch_dir: The directory containing the patches to apply
|
||||
pr_type: The type: branch (no PR created), draft or ready (regular PR created)
|
||||
fork_owner: The owner of the fork to push changes to (if different from the original repo owner)
|
||||
additional_message: The additional messages to post as a comment on the PR in json list format
|
||||
target_branch: The target branch to create the pull request against (defaults to repository default branch)
|
||||
reviewer: The username of the reviewer to assign
|
||||
reviewer: The GitHub or Gitlab username of the reviewer to assign
|
||||
pr_title: Custom title for the pull request (optional)
|
||||
base_domain: The base domain for the git server (defaults to "github.com" for GitHub, "gitlab.com" for GitLab, and "dev.azure.com" for Azure DevOps)
|
||||
base_domain: The base domain for the git server (defaults to "github.com" for GitHub and "gitlab.com" for GitLab)
|
||||
"""
|
||||
if pr_type not in ['branch', 'draft', 'ready']:
|
||||
raise ValueError(f'Invalid pr_type: {pr_type}')
|
||||
|
||||
# Determine default base_domain based on platform
|
||||
if base_domain is None:
|
||||
if platform == ProviderType.GITHUB:
|
||||
base_domain = 'github.com'
|
||||
elif platform == ProviderType.GITLAB:
|
||||
base_domain = 'gitlab.com'
|
||||
else: # platform == ProviderType.AZURE_DEVOPS
|
||||
base_domain = 'dev.azure.com'
|
||||
base_domain = 'github.com' if platform == ProviderType.GITHUB else 'gitlab.com'
|
||||
|
||||
# Create the appropriate handler based on platform
|
||||
handler = None
|
||||
if platform == ProviderType.GITHUB:
|
||||
handler = ServiceContextIssue(
|
||||
GithubIssueHandler(issue.owner, issue.repo, token, username, base_domain),
|
||||
None,
|
||||
)
|
||||
elif platform == ProviderType.GITLAB:
|
||||
else: # platform == Platform.GITLAB
|
||||
handler = ServiceContextIssue(
|
||||
GitlabIssueHandler(issue.owner, issue.repo, token, username, base_domain),
|
||||
None,
|
||||
)
|
||||
elif platform == ProviderType.AZURE_DEVOPS:
|
||||
handler = ServiceContextIssue(
|
||||
AzureDevOpsIssueHandler(
|
||||
issue.owner, issue.repo, token, username, base_domain
|
||||
),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported platform: {platform}')
|
||||
|
||||
# Create a new branch with a unique name
|
||||
base_branch_name = f'openhands-fix-issue-{issue.number}'
|
||||
|
||||
@@ -17,7 +17,7 @@ from openhands.integrations.utils import validate_provider_token
|
||||
|
||||
async def identify_token(token: str, base_domain: str | None) -> ProviderType:
|
||||
"""
|
||||
Identifies whether a token belongs to GitHub, GitLab, or Azure DevOps.
|
||||
Identifies whether a token belongs to GitHub or GitLab.
|
||||
Parameters:
|
||||
token (str): The personal access token to check.
|
||||
base_domain (str): Custom base domain for provider (e.g GitHub Enterprise)
|
||||
|
||||
@@ -411,7 +411,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
provider_domains = {
|
||||
ProviderType.GITHUB: 'github.com',
|
||||
ProviderType.GITLAB: 'gitlab.com',
|
||||
ProviderType.AZURE_DEVOPS: 'dev.azure.com',
|
||||
}
|
||||
|
||||
domain = provider_domains[provider]
|
||||
@@ -426,45 +425,10 @@ class Runtime(FileEditRuntimeMixin):
|
||||
if git_token:
|
||||
if provider == ProviderType.GITLAB:
|
||||
remote_repo_url = f'https://oauth2:{git_token.get_secret_value()}@{domain}/{selected_repository}.git'
|
||||
elif provider == ProviderType.AZURE_DEVOPS:
|
||||
# Azure DevOps URL format: https://token@dev.azure.com/organization/project/_git/repository
|
||||
# Extract organization from domain if it's a full URL
|
||||
if domain.startswith('https://dev.azure.com/'):
|
||||
org_name = domain.replace('https://dev.azure.com/', '').rstrip(
|
||||
'/'
|
||||
)
|
||||
base_domain = 'dev.azure.com'
|
||||
else:
|
||||
# If domain is just the host, we need to get organization from the token host
|
||||
token_host = git_provider_tokens[provider].host
|
||||
if token_host and token_host.startswith(
|
||||
'https://dev.azure.com/'
|
||||
):
|
||||
org_name = token_host.replace(
|
||||
'https://dev.azure.com/', ''
|
||||
).rstrip('/')
|
||||
base_domain = 'dev.azure.com'
|
||||
else:
|
||||
# Fallback: assume domain contains the organization
|
||||
org_name = domain.replace('dev.azure.com', '').strip('/')
|
||||
base_domain = 'dev.azure.com'
|
||||
|
||||
# Parse project/repo from selected_repository
|
||||
repo_parts = selected_repository.split('/')
|
||||
if len(repo_parts) == 2:
|
||||
project_name, repo_name = repo_parts
|
||||
remote_repo_url = f'https://{git_token.get_secret_value()}@{base_domain}/{org_name}/{project_name}/_git/{repo_name}'
|
||||
else:
|
||||
# Fallback to original format if parsing fails
|
||||
remote_repo_url = f'https://{git_token.get_secret_value()}@{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{git_token.get_secret_value()}@{domain}/{selected_repository}.git'
|
||||
else:
|
||||
if provider == ProviderType.AZURE_DEVOPS:
|
||||
# Public Azure DevOps repos (rare, but handle gracefully)
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
|
||||
@@ -683,8 +647,6 @@ fi
|
||||
provider = ProviderType.GITHUB
|
||||
elif 'gitlab.com' in repo_path:
|
||||
provider = ProviderType.GITLAB
|
||||
elif 'dev.azure.com' in repo_path:
|
||||
provider = ProviderType.AZURE_DEVOPS
|
||||
|
||||
# Add authentication if available
|
||||
if (
|
||||
@@ -696,8 +658,6 @@ fi
|
||||
if git_token:
|
||||
if provider == ProviderType.GITLAB:
|
||||
remote_url = f'https://oauth2:{git_token.get_secret_value()}@{repo_path.replace("gitlab.com/", "")}.git'
|
||||
elif provider == ProviderType.AZURE_DEVOPS:
|
||||
remote_url = f'https://{git_token.get_secret_value()}@{repo_path.replace("dev.azure.com/", "")}.git'
|
||||
else:
|
||||
remote_url = f'https://{git_token.get_secret_value()}@{repo_path.replace("github.com/", "")}.git'
|
||||
|
||||
@@ -713,7 +673,7 @@ fi
|
||||
the microagents from the ./microagents/ folder.
|
||||
|
||||
Args:
|
||||
selected_repository: The repository path (e.g., "github.com/acme-co/api" or "acme-co/api")
|
||||
selected_repository: The repository path (e.g., "github.com/acme-co/api")
|
||||
|
||||
Returns:
|
||||
A list of loaded microagents from the org/user level repository
|
||||
@@ -724,35 +684,14 @@ fi
|
||||
if len(repo_parts) < 2:
|
||||
return loaded_microagents
|
||||
|
||||
# Determine the provider and domain
|
||||
provider_domains = {
|
||||
ProviderType.GITHUB: 'github.com',
|
||||
ProviderType.GITLAB: 'gitlab.com',
|
||||
ProviderType.AZURE_DEVOPS: 'dev.azure.com',
|
||||
}
|
||||
|
||||
# First, try to extract domain from repository name if it includes one
|
||||
if len(repo_parts) > 2:
|
||||
domain = repo_parts[0]
|
||||
else:
|
||||
# Repository name doesn't include domain (e.g., "org/repo")
|
||||
# Try to determine provider from available tokens
|
||||
domain = 'github.com' # Default fallback
|
||||
|
||||
if self.git_provider_tokens:
|
||||
# If we only have one provider token, use that
|
||||
if len(self.git_provider_tokens) == 1:
|
||||
provider = next(iter(self.git_provider_tokens))
|
||||
domain = provider_domains.get(provider, 'github.com')
|
||||
else:
|
||||
# Multiple providers - would need additional logic to determine which one
|
||||
# For now, default to GitHub
|
||||
pass
|
||||
|
||||
# Extract the domain and org/user name
|
||||
domain = repo_parts[0] if len(repo_parts) > 2 else 'github.com'
|
||||
org_name = repo_parts[-2]
|
||||
|
||||
# Construct the org-level .openhands repo path
|
||||
org_openhands_repo = f'{domain}/{org_name}/.openhands'
|
||||
if domain not in org_openhands_repo:
|
||||
org_openhands_repo = f'github.com/{org_openhands_repo}'
|
||||
|
||||
self.log(
|
||||
'info',
|
||||
|
||||
@@ -245,10 +245,10 @@ class DockerRuntime(ActionExecutionClient):
|
||||
'mode': mount_mode,
|
||||
}
|
||||
logger.debug(
|
||||
f'Mount dir (sandbox.volumes): {host_path} to {container_path} with mode: {mount_mode}'
|
||||
f'Mount dir: {host_path} to {container_path} with mode: {mount_mode}'
|
||||
)
|
||||
|
||||
# Legacy mounting with workspace_* parameters
|
||||
# Legacy mounting with workspace_* parameters (deprecated)
|
||||
elif (
|
||||
self.config.workspace_mount_path is not None
|
||||
and self.config.workspace_mount_path_in_sandbox is not None
|
||||
@@ -261,7 +261,11 @@ class DockerRuntime(ActionExecutionClient):
|
||||
'mode': mount_mode,
|
||||
}
|
||||
logger.debug(
|
||||
f'Mount dir (legacy): {self.config.workspace_mount_path} with mode: {mount_mode}'
|
||||
f'Mount dir (using deprecated workspace_* variables): {self.config.workspace_mount_path} with mode: {mount_mode}'
|
||||
)
|
||||
logger.debug(
|
||||
'DEPRECATED: The workspace_* variables are deprecated and will be removed in a future version. '
|
||||
"Please use sandbox.volumes instead, e.g. 'SANDBOX_VOLUMES=/my/host/dir:/workspace:rw'"
|
||||
)
|
||||
|
||||
return volumes
|
||||
|
||||
@@ -363,7 +363,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
self._session_api_key = start_response['session_api_key']
|
||||
self.log(
|
||||
'debug',
|
||||
'Session API key set',
|
||||
f'Session API key setted',
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -59,7 +59,7 @@ class MCPProxyManager:
|
||||
"""
|
||||
if len(self.config['mcpServers']) == 0:
|
||||
logger.info(
|
||||
'No MCP servers configured for FastMCP Proxy, skipping initialization.'
|
||||
f"No MCP servers configured for FastMCP Proxy, skipping initialization."
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -70,7 +70,7 @@ class MCPProxyManager:
|
||||
api_key=self.api_key,
|
||||
)
|
||||
|
||||
logger.info('FastMCP Proxy initialized successfully')
|
||||
logger.info(f"FastMCP Proxy initialized successfully")
|
||||
|
||||
async def mount_to_app(
|
||||
self, app: FastAPI, allow_origins: Optional[list[str]] = None
|
||||
@@ -83,7 +83,9 @@ class MCPProxyManager:
|
||||
allow_origins: List of allowed origins for CORS
|
||||
"""
|
||||
if len(self.config['mcpServers']) == 0:
|
||||
logger.info('No MCP servers configured for FastMCP Proxy, skipping mount.')
|
||||
logger.info(
|
||||
f"No MCP servers configured for FastMCP Proxy, skipping mount."
|
||||
)
|
||||
return
|
||||
|
||||
if not self.proxy:
|
||||
@@ -99,7 +101,8 @@ class MCPProxyManager:
|
||||
app.routes.remove('/mcp')
|
||||
|
||||
app.mount('/', mcp_app)
|
||||
logger.info('Mounted FastMCP Proxy app at /mcp')
|
||||
logger.info(f"Mounted FastMCP Proxy app at /mcp")
|
||||
|
||||
|
||||
async def update_and_remount(
|
||||
self,
|
||||
@@ -119,7 +122,10 @@ class MCPProxyManager:
|
||||
tools: List of tool configurations
|
||||
allow_origins: List of allowed origins for CORS
|
||||
"""
|
||||
tools = {t.name: t.model_dump() for t in stdio_servers}
|
||||
tools = {
|
||||
t.name: t.model_dump()
|
||||
for t in stdio_servers
|
||||
}
|
||||
self.config['mcpServers'] = tools
|
||||
|
||||
del self.proxy
|
||||
|
||||
@@ -27,7 +27,7 @@ from openhands.llm.metrics import Metrics
|
||||
from openhands.utils.chunk_localizer import Chunk, get_top_k_chunk_matches
|
||||
|
||||
USER_MSG = """
|
||||
Code changes will be provided in the form of a draft. You will need to apply the draft to the original code.
|
||||
Code changes will be provided in the form of a draft. You will need to apply the draft to the original code.
|
||||
The original code will be enclosed within `<original_code>` tags.
|
||||
The draft will be enclosed within `<update_snippet>` tags.
|
||||
You need to output the update code within `<updated_code>` tags.
|
||||
@@ -48,8 +48,8 @@ def _extract_code(string: str) -> str | None:
|
||||
|
||||
content = str(matches[0])
|
||||
if content.startswith('#EDIT:'):
|
||||
# Remove first line
|
||||
content = content[content.find('\n') + 1 :]
|
||||
#Remove first line
|
||||
content = content[content.find('\n') + 1:]
|
||||
return content
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import random
|
||||
import socket
|
||||
import time
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
def check_port_available(port: int) -> bool:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user