mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81c5c204db | |||
| 8ae2fb636e | |||
| de75bd0690 | |||
| 2fb45d410d | |||
| 8300cf0436 | |||
| 7dd2bc569f | |||
| 6e1fae29c9 | |||
| 19525a487c | |||
| 7d0405282a | |||
| 92c166551f | |||
| ebb68b33db | |||
| 37c46f1ed8 | |||
| ac5190c283 | |||
| ed3916b79b | |||
| 27a647cd3e | |||
| 42a536d450 | |||
| 41e564dc41 | |||
| e979f51ea5 | |||
| 425ccb0fbb | |||
| 7e4c1c733b | |||
| ffd472d6b8 | |||
| 2f2ea9ec91 | |||
| 6a6ce5f3ee | |||
| 907c65cc00 | |||
| a6d1a4c98f | |||
| a60ee09881 | |||
| 246107c618 | |||
| 5fa18511b3 | |||
| a482182a9e | |||
| 58d22a1905 | |||
| 17bbfa29a1 | |||
| 5fe116cfb1 | |||
| e9637d40b9 | |||
| 6de177521f | |||
| 9d36b80b96 | |||
| b11e905988 | |||
| 39e5311233 | |||
| 651ed1c3c8 | |||
| e27c2e9c99 | |||
| cfe222e1d5 | |||
| c872af4658 | |||
| 99fa6c6a4a | |||
| 3946f813a4 | |||
| 455e667739 | |||
| 2874041381 | |||
| 279e1d7abc | |||
| a7e4a7aa63 | |||
| 2466d903df | |||
| 424cdf121a | |||
| 6972f4806f | |||
| 78cc552e3a |
@@ -1 +0,0 @@
|
||||
The files in this directory configure a development container for GitHub Codespaces.
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "OpenHands Codespaces",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal",
|
||||
"customizations":{
|
||||
"vscode":{
|
||||
"extensions": [
|
||||
"ms-python.python"
|
||||
]
|
||||
}
|
||||
},
|
||||
"onCreateCommand": "sh ./.devcontainer/on_create.sh",
|
||||
"postCreateCommand": "make build",
|
||||
"postStartCommand": "USE_HOST_NETWORK=True nohup bash -c 'make run &'"
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
sudo apt update
|
||||
sudo apt install -y netcat
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt install -y python3.12
|
||||
curl -sSL https://install.python-poetry.org | python3.12 -
|
||||
@@ -18,7 +18,7 @@ updates:
|
||||
- "chromadb"
|
||||
browsergym:
|
||||
patterns:
|
||||
- "browsergym"
|
||||
- "browsergym*"
|
||||
security-all:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
|
||||
@@ -68,6 +68,9 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: "Set up docker layer caching"
|
||||
uses: satackey/action-docker-layer-caching@v0.0.11
|
||||
continue-on-error: true
|
||||
- name: Build and push app image
|
||||
if: "!github.event.pull_request.head.repo.fork"
|
||||
run: |
|
||||
|
||||
@@ -239,7 +239,8 @@ jobs:
|
||||
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
|
||||
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||
--pr-type draft | tee pr_result.txt && \
|
||||
--pr-type draft \
|
||||
--reviewer ${{ github.actor }} | tee pr_result.txt && \
|
||||
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
|
||||
else
|
||||
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
|
||||
@@ -29,6 +29,11 @@ call APIs, and yes—even copy code snippets from StackOverflow.
|
||||
|
||||
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Using OpenHands for work? We'd love to chat! Fill out
|
||||
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
|
||||
|
||||

|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
@@ -217,6 +217,9 @@ llm_config = 'gpt3'
|
||||
# Use host network
|
||||
#use_host_network = false
|
||||
|
||||
# runtime extra build args
|
||||
#runtime_extra_build_args = ["--network=host", "--add-host=host.docker.internal:host-gateway"]
|
||||
|
||||
# Enable auto linting after editing
|
||||
#enable_auto_lint = false
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Develop in Docker
|
||||
|
||||
> [!WARNING]
|
||||
> This is not officially supported and may not work.
|
||||
|
||||
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:
|
||||
|
||||
```bash
|
||||
|
||||
-1
@@ -9,7 +9,6 @@ Si vous trouvez plus d'informations ou une solution de contournement pour l'un d
|
||||
:::tip
|
||||
OpenHands ne prend en charge Windows que via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
Veuillez vous assurer d'exécuter toutes les commandes à l'intérieur de votre terminal WSL.
|
||||
Consultez les [Notes pour les utilisateurs de WSL sur Windows](troubleshooting/windows) pour des guides de dépannage.
|
||||
:::
|
||||
|
||||
## Problèmes courants
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
|
||||
# Notes pour les utilisateurs de WSL sur Windows
|
||||
|
||||
OpenHands ne prend en charge Windows que via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
Veuillez vous assurer d'exécuter toutes les commandes dans votre terminal WSL.
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Recommandation : Ne pas exécuter en tant qu'utilisateur root
|
||||
|
||||
Pour des raisons de sécurité, il est fortement recommandé de ne pas exécuter OpenHands en tant qu'utilisateur root, mais en tant qu'utilisateur avec un UID non nul.
|
||||
|
||||
Références :
|
||||
|
||||
* [Pourquoi il est mauvais de se connecter en tant que root](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
|
||||
* [Définir l'utilisateur par défaut dans WSL](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
|
||||
Astuce concernant la 2ème référence : pour les utilisateurs d'Ubuntu, la commande pourrait en fait être "ubuntupreview" au lieu de "ubuntu".
|
||||
|
||||
---
|
||||
### Erreur : 'docker' n'a pas pu être trouvé dans cette distribution WSL 2.
|
||||
|
||||
Si vous utilisez Docker Desktop, assurez-vous de le démarrer avant d'appeler toute commande docker depuis WSL.
|
||||
Docker doit également avoir l'option d'intégration WSL activée.
|
||||
|
||||
---
|
||||
### Installation de Poetry
|
||||
|
||||
* Si vous rencontrez des problèmes pour exécuter Poetry même après l'avoir installé pendant le processus de build, vous devrez peut-être ajouter son chemin binaire à votre environnement :
|
||||
|
||||
```sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
* Si make build s'arrête sur une erreur comme celle-ci :
|
||||
|
||||
```sh
|
||||
ModuleNotFoundError: no module named <module-name>
|
||||
```
|
||||
|
||||
Cela pourrait être un problème avec le cache de Poetry.
|
||||
Essayez d'exécuter ces 2 commandes l'une après l'autre :
|
||||
|
||||
```sh
|
||||
rm -r ~/.cache/pypoetry
|
||||
make build
|
||||
```
|
||||
|
||||
---
|
||||
### L'objet NoneType n'a pas d'attribut 'request'
|
||||
|
||||
Si vous rencontrez des problèmes liés au réseau, tels que `NoneType object has no attribute 'request'` lors de l'exécution de `make run`, vous devrez peut-être configurer les paramètres réseau de WSL2. Suivez ces étapes :
|
||||
|
||||
* Ouvrez ou créez le fichier `.wslconfig` situé à `C:\Users\%username%\.wslconfig` sur votre machine hôte Windows.
|
||||
* Ajoutez la configuration suivante au fichier `.wslconfig` :
|
||||
|
||||
```sh
|
||||
[wsl2]
|
||||
networkingMode=mirrored
|
||||
localhostForwarding=true
|
||||
```
|
||||
|
||||
* Enregistrez le fichier `.wslconfig`.
|
||||
* Redémarrez complètement WSL2 en quittant toutes les instances WSL2 en cours d'exécution et en exécutant la commande `wsl --shutdown` dans votre invite de commande ou terminal.
|
||||
* Après avoir redémarré WSL, essayez d'exécuter à nouveau `make run`.
|
||||
Le problème de réseau devrait être résolu.
|
||||
-1
@@ -7,7 +7,6 @@
|
||||
:::tip
|
||||
OpenHands 仅通过 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) 支持 Windows。
|
||||
请确保在您的 WSL 终端内运行所有命令。
|
||||
查看 [Windows 用户的 WSL 注意事项](troubleshooting/windows) 以获取一些故障排除指南。
|
||||
:::
|
||||
|
||||
## 常见问题
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
以下是翻译后的内容:
|
||||
|
||||
# 针对 Windows 上 WSL 用户的注意事项
|
||||
|
||||
OpenHands 仅通过 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) 支持 Windows。
|
||||
请确保在您的 WSL 终端内运行所有命令。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 建议: 不要以 root 用户身份运行
|
||||
|
||||
出于安全原因,强烈建议不要以 root 用户身份运行 OpenHands,而是以具有非零 UID 的用户身份运行。
|
||||
|
||||
参考:
|
||||
|
||||
* [为什么以 root 身份登录不好](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
|
||||
* [在 WSL 中设置默认用户](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
|
||||
关于第二个参考的提示:对于 Ubuntu 用户,命令实际上可能是 "ubuntupreview" 而不是 "ubuntu"。
|
||||
|
||||
---
|
||||
### 错误: 在此 WSL 2 发行版中找不到 'docker'。
|
||||
|
||||
如果您正在使用 Docker Desktop,请确保在从 WSL 内部调用任何 docker 命令之前启动它。
|
||||
Docker 还需要激活 WSL 集成选项。
|
||||
|
||||
---
|
||||
### Poetry 安装
|
||||
|
||||
* 如果您在构建过程中安装 Poetry 后仍然面临运行 Poetry 的问题,您可能需要将其二进制路径添加到环境中:
|
||||
|
||||
```sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
* 如果 make build 在如下错误上停止:
|
||||
|
||||
```sh
|
||||
ModuleNotFoundError: no module named <module-name>
|
||||
```
|
||||
|
||||
这可能是 Poetry 缓存的问题。
|
||||
尝试依次运行这两个命令:
|
||||
|
||||
```sh
|
||||
rm -r ~/.cache/pypoetry
|
||||
make build
|
||||
```
|
||||
|
||||
---
|
||||
### NoneType 对象没有属性 'request'
|
||||
|
||||
如果您在执行 `make run` 时遇到与网络相关的问题,例如 `NoneType 对象没有属性 'request'`,您可能需要配置 WSL2 网络设置。请按照以下步骤操作:
|
||||
|
||||
* 在 Windows 主机上打开或创建位于 `C:\Users\%username%\.wslconfig` 的 `.wslconfig` 文件。
|
||||
* 将以下配置添加到 `.wslconfig` 文件中:
|
||||
|
||||
```sh
|
||||
[wsl2]
|
||||
networkingMode=mirrored
|
||||
localhostForwarding=true
|
||||
```
|
||||
|
||||
* 保存 `.wslconfig` 文件。
|
||||
* 通过退出任何正在运行的 WSL2 实例并在命令提示符或终端中执行 `wsl --shutdown` 命令来完全重启 WSL2。
|
||||
* 重新启动 WSL 后,再次尝试执行 `make run`。
|
||||
网络问题应该得到解决。
|
||||
@@ -23,10 +23,75 @@ OpenHands provides a user-friendly Graphical User Interface (GUI) mode for inter
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
1. Locally (OSS): The user directly inputs their GitHub token.
|
||||
2. Online (SaaS): The token is obtained through GitHub OAuth authentication.
|
||||
1. **Locally (OSS)**: The user directly inputs their GitHub token
|
||||
2. **Online (SaaS)**: The token is obtained through GitHub OAuth authentication
|
||||
|
||||
When you reach the `/app` route, the app checks if a token is present. If it finds one, it sets it in the environment for the agent to use.
|
||||
#### Setting Up a Local GitHub Token
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic)
|
||||
- Click "Generate new token (classic)"
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `workflow` (Update GitHub Action workflows)
|
||||
- `read:org` (Read organization data)
|
||||
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon) in the top right
|
||||
- Navigate to the "GitHub" section
|
||||
- Paste your token in the "GitHub Token" field
|
||||
- Click "Save" to apply the changes
|
||||
|
||||
#### Organizational Token Policies
|
||||
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
1. **Check Organization Requirements**:
|
||||
- Organization admins may enforce specific token policies
|
||||
- Some organizations require tokens to be created with SSO enabled
|
||||
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization)
|
||||
|
||||
2. **Verify Organization Access**:
|
||||
- Go to your token settings on GitHub
|
||||
- Look for the organization under "Organization access"
|
||||
- If required, click "Enable SSO" next to your organization
|
||||
- Complete the SSO authorization process
|
||||
|
||||
#### OAuth Authentication (Online Mode)
|
||||
|
||||
When using OpenHands in online mode, the GitHub OAuth flow:
|
||||
|
||||
1. Requests the following permissions:
|
||||
- Repository access (read/write)
|
||||
- Workflow management
|
||||
- Organization read access
|
||||
|
||||
2. Authentication steps:
|
||||
- Click "Sign in with GitHub" when prompted
|
||||
- Review the requested permissions
|
||||
- Authorize OpenHands to access your GitHub account
|
||||
- If using an organization, authorize organization access if prompted
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings
|
||||
- Check that the token hasn't expired
|
||||
- Verify the token has the required scopes
|
||||
- Try regenerating the token
|
||||
|
||||
2. **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled
|
||||
- Verify organization membership
|
||||
- Contact organization admin if token policies are blocking access
|
||||
|
||||
3. **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid
|
||||
- Try accessing a repository to confirm permissions
|
||||
- Check the browser console for any error messages
|
||||
- Use the "Test Connection" button in settings if available
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ docker run -it \
|
||||
--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.15 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Persisting Session Data
|
||||
|
||||
Using the standard installation, the session data is stored in memory. Currently, if OpenHands' service is restarted,
|
||||
previous sessions become invalid (a new secret is generated) and thus not recoverable.
|
||||
|
||||
## How to Persist Session Data
|
||||
|
||||
### Development Workflow
|
||||
In the `config.toml` file, specify the following:
|
||||
```
|
||||
[core]
|
||||
...
|
||||
file_store="local"
|
||||
file_store_path="/absolute/path/to/openhands/cache/directory"
|
||||
jwt_secret="secretpass"
|
||||
```
|
||||
@@ -0,0 +1,213 @@
|
||||
# Micro-Agents
|
||||
|
||||
OpenHands uses specialized micro-agents to handle specific tasks and contexts efficiently. These micro-agents are small, focused components that provide specialized behavior and knowledge for particular scenarios.
|
||||
|
||||
## Overview
|
||||
|
||||
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
|
||||
|
||||
- A unique name
|
||||
- The agent type (typically CodeActAgent)
|
||||
- Trigger keywords that activate the agent
|
||||
- Specific instructions and capabilities
|
||||
|
||||
## Available Micro-Agents
|
||||
|
||||
### GitHub Agent
|
||||
**File**: `github.md`
|
||||
**Triggers**: `github`, `git`
|
||||
|
||||
The GitHub agent specializes in GitHub API interactions and repository management. It:
|
||||
- Has access to a `GITHUB_TOKEN` for API authentication
|
||||
- Follows strict guidelines for repository interactions
|
||||
- Handles branch management and pull requests
|
||||
- Uses the GitHub API instead of web browser interactions
|
||||
|
||||
Key features:
|
||||
- Branch protection (prevents direct pushes to main/master)
|
||||
- Automated PR creation
|
||||
- Git configuration management
|
||||
- API-first approach for GitHub operations
|
||||
|
||||
### NPM Agent
|
||||
**File**: `npm.md`
|
||||
**Triggers**: `npm`
|
||||
|
||||
Specializes in handling npm package management with specific focus on:
|
||||
- Non-interactive shell operations
|
||||
- Automated confirmation handling using Unix 'yes' command
|
||||
- Package installation automation
|
||||
|
||||
### Custom Micro-Agents
|
||||
|
||||
You can create your own micro-agents by adding new markdown files to the micro-agents directory. Each file should follow this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: agent_name
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- trigger_word1
|
||||
- trigger_word2
|
||||
---
|
||||
|
||||
Instructions and capabilities for the micro-agent...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
When working with micro-agents:
|
||||
|
||||
1. **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent
|
||||
2. **Follow Agent Guidelines**: Each agent has specific instructions and limitations - respect these for optimal results
|
||||
3. **API-First Approach**: When available, use API endpoints rather than web interfaces
|
||||
4. **Automation Friendly**: Design commands that work well in non-interactive environments
|
||||
|
||||
## Integration
|
||||
|
||||
Micro-agents are automatically integrated into OpenHands' workflow. They:
|
||||
- Monitor incoming commands for their trigger words
|
||||
- Activate when relevant triggers are detected
|
||||
- Apply their specialized knowledge and capabilities
|
||||
- Follow their specific guidelines and restrictions
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# GitHub agent example
|
||||
git checkout -b feature-branch
|
||||
git commit -m "Add new feature"
|
||||
git push origin feature-branch
|
||||
|
||||
# NPM agent example
|
||||
yes | npm install package-name
|
||||
```
|
||||
|
||||
For more information about specific agents, refer to their individual documentation files in the micro-agents directory.
|
||||
|
||||
## Contributing a Micro-Agent
|
||||
|
||||
To contribute a new micro-agent to OpenHands, follow these guidelines:
|
||||
|
||||
### 1. Planning Your Micro-Agent
|
||||
|
||||
Before creating a micro-agent, consider:
|
||||
- What specific problem or use case will it address?
|
||||
- What unique capabilities or knowledge should it have?
|
||||
- What trigger words make sense for activating it?
|
||||
- What constraints or guidelines should it follow?
|
||||
|
||||
### 2. File Structure
|
||||
|
||||
Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
|
||||
|
||||
### 3. Required Components
|
||||
|
||||
Your micro-agent file must include:
|
||||
|
||||
1. **Front Matter**: YAML metadata at the start of the file:
|
||||
```markdown
|
||||
---
|
||||
name: your_agent_name
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- trigger_word1
|
||||
- trigger_word2
|
||||
---
|
||||
```
|
||||
|
||||
2. **Instructions**: Clear, specific guidelines for the agent's behavior:
|
||||
```markdown
|
||||
You are responsible for [specific task/domain].
|
||||
|
||||
Key responsibilities:
|
||||
1. [Responsibility 1]
|
||||
2. [Responsibility 2]
|
||||
|
||||
Guidelines:
|
||||
- [Guideline 1]
|
||||
- [Guideline 2]
|
||||
|
||||
Examples of usage:
|
||||
[Example 1]
|
||||
[Example 2]
|
||||
```
|
||||
|
||||
### 4. Best Practices for Micro-Agent Development
|
||||
|
||||
1. **Clear Scope**: Keep the agent focused on a specific domain or task
|
||||
2. **Explicit Instructions**: Provide clear, unambiguous guidelines
|
||||
3. **Useful Examples**: Include practical examples of common use cases
|
||||
4. **Safety First**: Include necessary warnings and constraints
|
||||
5. **Integration Awareness**: Consider how the agent interacts with other components
|
||||
|
||||
### 5. Testing Your Micro-Agent
|
||||
|
||||
Before submitting:
|
||||
1. Test the agent with various prompts
|
||||
2. Verify trigger words activate the agent correctly
|
||||
3. Ensure instructions are clear and comprehensive
|
||||
4. Check for potential conflicts with existing agents
|
||||
|
||||
### 6. Example Implementation
|
||||
|
||||
Here's a template for a new micro-agent:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: docker
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- docker
|
||||
- container
|
||||
---
|
||||
|
||||
You are responsible for Docker container management and Dockerfile creation.
|
||||
|
||||
Key responsibilities:
|
||||
1. Create and modify Dockerfiles
|
||||
2. Manage container lifecycle
|
||||
3. Handle Docker Compose configurations
|
||||
|
||||
Guidelines:
|
||||
- Always use official base images when possible
|
||||
- Include necessary security considerations
|
||||
- Follow Docker best practices for layer optimization
|
||||
|
||||
Examples:
|
||||
1. Creating a Dockerfile:
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
2. Docker Compose usage:
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
```
|
||||
|
||||
Remember to:
|
||||
- Validate Dockerfile syntax
|
||||
- Check for security vulnerabilities
|
||||
- Optimize for build time and image size
|
||||
```
|
||||
|
||||
### 7. Submission Process
|
||||
|
||||
1. Create your micro-agent file in the correct directory
|
||||
2. Test thoroughly
|
||||
3. Submit a pull request with:
|
||||
- The new micro-agent file
|
||||
- Updated documentation if needed
|
||||
- Description of the agent's purpose and capabilities
|
||||
|
||||
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed agents can significantly improve the system's ability to handle specialized tasks.
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
When working with OpenHands AI software developer, it's crucial to provide clear and effective prompts. This guide outlines best practices for creating prompts that will yield the most accurate and useful responses.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Characteristics of Good Prompts](#characteristics-of-good-prompts)
|
||||
- [Customizing Prompts for your Project](#customizing-prompts-for-your-project)
|
||||
|
||||
## Characteristics of Good Prompts
|
||||
|
||||
Good prompts are:
|
||||
@@ -39,3 +44,63 @@ Good prompts are:
|
||||
Remember, the more precise and informative your prompt is, the better the AI can assist you in developing or modifying the OpenHands software.
|
||||
|
||||
See [Getting Started with OpenHands](./getting-started) for more examples of helpful prompts.
|
||||
|
||||
## Customizing Prompts for your Project
|
||||
|
||||
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines. This section explains how to optimize OpenHands for your project.
|
||||
|
||||
### Repository Configuration
|
||||
|
||||
You can customize OpenHands' behavior for your repository by creating a `.openhands_instructions` file in your repository's root directory. This file should contain:
|
||||
|
||||
1. **Repository Overview**: A brief description of your project's purpose and architecture
|
||||
2. **Directory Structure**: Key directories and their purposes
|
||||
3. **Development Guidelines**: Project-specific coding standards and practices
|
||||
4. **Testing Requirements**: How to run tests and what types of tests are required
|
||||
5. **Setup Instructions**: Steps needed to build and run the project
|
||||
|
||||
Example `.openhands_instructions` file:
|
||||
```
|
||||
Repository: MyProject
|
||||
Description: A web application for task management
|
||||
|
||||
Directory Structure:
|
||||
- src/: Main application code
|
||||
- tests/: Test files
|
||||
- docs/: Documentation
|
||||
|
||||
Setup:
|
||||
- Run `npm install` to install dependencies
|
||||
- Use `npm run dev` for development
|
||||
- Run `npm test` for testing
|
||||
|
||||
Guidelines:
|
||||
- Follow ESLint configuration
|
||||
- Write tests for all new features
|
||||
- Use TypeScript for new code
|
||||
```
|
||||
|
||||
### Customizing Prompts
|
||||
|
||||
When working with a customized repository:
|
||||
|
||||
1. **Reference Project Standards**: Mention specific coding standards or patterns used in your project
|
||||
2. **Include Context**: Reference relevant documentation or existing implementations
|
||||
3. **Specify Testing Requirements**: Include project-specific testing requirements in your prompts
|
||||
|
||||
Example customized prompt:
|
||||
```
|
||||
Add a new task completion feature to src/components/TaskList.tsx following our existing component patterns.
|
||||
Include unit tests in tests/components/ and update the documentation in docs/features/.
|
||||
The component should use our shared styling from src/styles/components.
|
||||
```
|
||||
|
||||
### Best Practices for Repository Customization
|
||||
|
||||
1. **Keep Instructions Updated**: Regularly update your `.openhands_instructions` file as your project evolves
|
||||
2. **Be Specific**: Include specific paths, patterns, and requirements unique to your project
|
||||
3. **Document Dependencies**: List all tools and dependencies required for development
|
||||
4. **Include Examples**: Provide examples of good code patterns from your project
|
||||
5. **Specify Conventions**: Document naming conventions, file organization, and code style preferences
|
||||
|
||||
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.
|
||||
|
||||
@@ -1,180 +1,44 @@
|
||||
# 🚧 Troubleshooting
|
||||
|
||||
There are some error messages that frequently get reported by users.
|
||||
We'll try to make the install process easier, but for now you can look for your error message below and see if there are any workarounds.
|
||||
If you find more information or a workaround for one of these issues, please open a *PR* to add details to this file.
|
||||
|
||||
:::tip
|
||||
OpenHands only supports Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
Please be sure to run all commands inside your WSL terminal.
|
||||
Check out [Notes for WSL on Windows Users](troubleshooting/windows) for some troubleshooting guides.
|
||||
OpenHands only supports Windows via WSL. Please be sure to run all commands inside your WSL terminal.
|
||||
:::
|
||||
|
||||
## Common Issues
|
||||
### Launch docker client failed
|
||||
|
||||
* [Unable to connect to Docker](#unable-to-connect-to-docker)
|
||||
* [404 Resource not found](#404-resource-not-found)
|
||||
* [`make build` getting stuck on package installations](#make-build-getting-stuck-on-package-installations)
|
||||
* [Sessions are not restored](#sessions-are-not-restored)
|
||||
* [Connection to host.docker.internal timed out](#connection-to-host-docker-internal-timed-out)
|
||||
**Description**
|
||||
|
||||
### Unable to connect to Docker
|
||||
|
||||
[GitHub Issue](https://github.com/All-Hands-AI/OpenHands/issues/1226)
|
||||
|
||||
**Symptoms**
|
||||
|
||||
```bash
|
||||
Error creating controller. Please check Docker is running and visit `https://docs.all-hands.dev/modules/usage/troubleshooting` for more debugging information.
|
||||
When running OpenHands, the following error is seen:
|
||||
```
|
||||
Launch docker client failed. Please make sure you have installed docker and started docker desktop/daemon.
|
||||
```
|
||||
|
||||
```bash
|
||||
docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))
|
||||
```
|
||||
|
||||
**Details**
|
||||
|
||||
OpenHands uses a Docker container to do its work safely, without potentially breaking your machine.
|
||||
|
||||
**Workarounds**
|
||||
|
||||
* Run `docker ps` to ensure that docker is running
|
||||
* Make sure you don't need `sudo` to run docker [see here](https://www.baeldung.com/linux/docker-run-without-sudo)
|
||||
* If you are on a Mac, check the [permissions requirements](https://docs.docker.com/desktop/mac/permission-requirements/) and in particular consider enabling the `Allow the default Docker socket to be used` under `Settings > Advanced` in Docker Desktop.
|
||||
* In addition, upgrade your Docker to the latest version under `Check for Updates`
|
||||
**Resolution**
|
||||
|
||||
Try these in order:
|
||||
* Confirm `docker` is running on your system. You should be able to run `docker ps` in the terminal successfully.
|
||||
* If using Docker Desktop, ensure `Settings > Advanced > Allow the default Docker socket to be used` is enabled.
|
||||
* Depending on your configuration you may need `Settings > Resources > Network > Enable host networking` enabled in Docker Desktop.
|
||||
* Reinstall Docker Desktop.
|
||||
---
|
||||
### `404 Resource not found`
|
||||
|
||||
**Symptoms**
|
||||
# Development Workflow Specific
|
||||
### Error building runtime docker image
|
||||
|
||||
```python
|
||||
Traceback (most recent call last):
|
||||
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 414, in completion
|
||||
raise e
|
||||
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 373, in completion
|
||||
response = openai_client.chat.completions.create(**data, timeout=timeout) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/app/.venv/lib/python3.12/site-packages/openai/_utils/_utils.py", line 277, in wrapper
|
||||
return func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/app/.venv/lib/python3.12/site-packages/openai/resources/chat/completions.py", line 579, in create
|
||||
return self._post(
|
||||
^^^^^^^^^^^
|
||||
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1232, in post
|
||||
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 921, in request
|
||||
return self._request(
|
||||
^^^^^^^^^^^^^^
|
||||
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1012, in _request
|
||||
raise self._make_status_error_from_response(err.response) from None
|
||||
openai.NotFoundError: Error code: 404 - {'error': {'code': '404', 'message': 'Resource not found'}}
|
||||
**Description**
|
||||
|
||||
Attempts to start a new session fail, and errors with terms like the following appear in the logs:
|
||||
```
|
||||
debian-security bookworm-security
|
||||
InRelease At least one invalid signature was encountered.
|
||||
```
|
||||
|
||||
**Details**
|
||||
This seems to happen when the hash of an existing external library changes and your local docker instance has
|
||||
cached a previous version. To work around this, please try the following:
|
||||
|
||||
This happens when LiteLLM (our library for connecting to different LLM providers) can't find
|
||||
the API endpoint you're trying to connect to. Most often this happens for Azure or ollama users.
|
||||
|
||||
**Workarounds**
|
||||
|
||||
* Check that you've set `LLM_BASE_URL` properly
|
||||
* Check that the model is set properly, based on the [LiteLLM docs](https://docs.litellm.ai/docs/providers)
|
||||
* If you're running inside the UI, be sure to set the `model` in the settings modal
|
||||
* If you're running headless (via main.py) be sure to set `LLM_MODEL` in your env/config
|
||||
* Make sure you've followed any special instructions for your LLM provider
|
||||
* [Azure](/modules/usage/llms/azure-llms)
|
||||
* [Google](/modules/usage/llms/google-llms)
|
||||
* Make sure your API key is correct
|
||||
* See if you can connect to the LLM using `curl`
|
||||
* Try [connecting via LiteLLM directly](https://github.com/BerriAI/litellm) to test your setup
|
||||
|
||||
---
|
||||
### `make build` getting stuck on package installations
|
||||
|
||||
**Symptoms**
|
||||
|
||||
Package installation stuck on `Pending...` without any error message:
|
||||
|
||||
```bash
|
||||
Package operations: 286 installs, 0 updates, 0 removals
|
||||
|
||||
- Installing certifi (2024.2.2): Pending...
|
||||
- Installing h11 (0.14.0): Pending...
|
||||
- Installing idna (3.7): Pending...
|
||||
- Installing sniffio (1.3.1): Pending...
|
||||
- Installing typing-extensions (4.11.0): Pending...
|
||||
```
|
||||
|
||||
**Details**
|
||||
|
||||
In rare cases, `make build` can seemingly get stuck on package installations
|
||||
without any error message.
|
||||
|
||||
**Workarounds**
|
||||
|
||||
The package installer Poetry may miss a configuration setting for where credentials are to be looked up (keyring).
|
||||
|
||||
First check with `env` if a value for `PYTHON_KEYRING_BACKEND` exists.
|
||||
If not, run the below command to set it to a known value and retry the build:
|
||||
|
||||
```bash
|
||||
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
|
||||
```
|
||||
|
||||
---
|
||||
### Sessions are not restored
|
||||
|
||||
**Symptoms**
|
||||
|
||||
OpenHands usually asks whether to resume or start a new session when opening the UI.
|
||||
But clicking "Resume" still starts a fresh new chat.
|
||||
|
||||
**Details**
|
||||
|
||||
With a standard installation as of today session data is stored in memory.
|
||||
Currently, if OpenHands's service is restarted, previous sessions become
|
||||
invalid (a new secret is generated) and thus not recoverable.
|
||||
|
||||
**Workarounds**
|
||||
|
||||
* Change configuration to make sessions persistent by editing the `config.toml`
|
||||
file (in OpenHands's root folder) by specifying a `file_store` and an
|
||||
absolute `file_store_path`:
|
||||
|
||||
```toml
|
||||
file_store="local"
|
||||
file_store_path="/absolute/path/to/openhands/cache/directory"
|
||||
```
|
||||
|
||||
* Add a fixed jwt secret in your .bashrc, like below, so that previous session id's
|
||||
should stay accepted.
|
||||
|
||||
```bash
|
||||
EXPORT JWT_SECRET=A_CONST_VALUE
|
||||
```
|
||||
|
||||
---
|
||||
### Connection to host docker internal timed out
|
||||
|
||||
**Symptoms**
|
||||
|
||||
When you start the server using the docker command from the main [README](https://github.com/All-Hands-AI/OpenHands/README.md), you get a long timeout
|
||||
followed by the a stack trace containing messages like:
|
||||
|
||||
* `Connection to host.docker.internal timed out. (connect timeout=310)`
|
||||
* `Max retries exceeded with url: /alive`
|
||||
|
||||
**Details**
|
||||
|
||||
If Docker Engine is installed rather than Docker Desktop, the main command will not work as expected.
|
||||
Docker Desktop includes easy DNS configuration for connecting processes running in different containers
|
||||
which OpenHands makes use of when the main server is running inside a docker container.
|
||||
(Further details: https://forums.docker.com/t/difference-between-docker-desktop-and-docker-engine/124612)
|
||||
|
||||
**Workarounds**
|
||||
|
||||
* [Install Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
* Run OpenHands in [Development Mode](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md),
|
||||
So that the main server is not run inside a container, but still creates dockerized runtime sandboxes.
|
||||
* Stop any containers where the name has the prefix `openhands-runtime-` :
|
||||
`docker ps --filter name=openhands-runtime- --filter status=running -aq | xargs docker stop`
|
||||
* Remove any containers where the name has the prefix `openhands-runtime-` :
|
||||
`docker rmi $(docker images --filter name=openhands-runtime- -q --no-trunc)`
|
||||
* Stop and Remove any containers / images where the name has the prefix `openhands-runtime-`
|
||||
* Prune containers / images : `docker container prune -f && docker image prune -f`
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# Notes for WSL on Windows Users
|
||||
|
||||
OpenHands only supports Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
Please be sure to run all commands inside your WSL terminal.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Recommendation: Do not run as root user
|
||||
|
||||
For security reasons, it is highly recommended to not run OpenHands as the root user, but a user with a non-zero UID.
|
||||
|
||||
References:
|
||||
|
||||
* [Why it is bad to login as root](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
|
||||
* [Set default user in WSL](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
|
||||
Hint about the 2nd reference: for Ubuntu users, the command could actually be "ubuntupreview" instead of "ubuntu".
|
||||
|
||||
---
|
||||
### Error: 'docker' could not be found in this WSL 2 distro.
|
||||
|
||||
If you are using Docker Desktop, make sure to start it before calling any docker command from inside WSL.
|
||||
Docker also needs to have the WSL integration option activated.
|
||||
|
||||
---
|
||||
### Poetry Installation
|
||||
|
||||
* If you face issues running Poetry even after installing it during the build process, you may need to add its binary path to your environment:
|
||||
|
||||
```sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
* If make build stops on an error like this:
|
||||
|
||||
```sh
|
||||
ModuleNotFoundError: no module named <module-name>
|
||||
```
|
||||
|
||||
This could be an issue with Poetry's cache.
|
||||
Try to run these 2 commands after another:
|
||||
|
||||
```sh
|
||||
rm -r ~/.cache/pypoetry
|
||||
make build
|
||||
```
|
||||
|
||||
---
|
||||
### NoneType object has no attribute 'request'
|
||||
|
||||
If you are experiencing issues related to networking, such as `NoneType object has no attribute 'request'` when executing `make run`, you may need to configure your WSL2 networking settings. Follow these steps:
|
||||
|
||||
* Open or create the `.wslconfig` file located at `C:\Users\%username%\.wslconfig` on your Windows host machine.
|
||||
* Add the following configuration to the `.wslconfig` file:
|
||||
|
||||
```sh
|
||||
[wsl2]
|
||||
networkingMode=mirrored
|
||||
localhostForwarding=true
|
||||
```
|
||||
|
||||
* Save the `.wslconfig` file.
|
||||
* Restart WSL2 completely by exiting any running WSL2 instances and executing the command `wsl --shutdown` in your command prompt or terminal.
|
||||
* After restarting WSL, attempt to execute `make run` again.
|
||||
The networking issue should be resolved.
|
||||
@@ -1,71 +0,0 @@
|
||||
# ⬆️ Upgrade Guide
|
||||
|
||||
## 0.8.0 (2024-07-13)
|
||||
|
||||
### Config breaking changes
|
||||
|
||||
In this release we introduced a few breaking changes to backend configurations.
|
||||
If you have only been using OpenHands via frontend (web GUI), nothing needs
|
||||
to be taken care of.
|
||||
|
||||
Here's a list of breaking changes in configs. They only apply to users who
|
||||
use OpenHands CLI via `main.py`. For more detail, see [#2756](https://github.com/All-Hands-AI/OpenHands/pull/2756).
|
||||
|
||||
#### Removal of --model-name option from main.py
|
||||
|
||||
Please note that `--model-name`, or `-m` option, no longer exists. You should set up the LLM
|
||||
configs in `config.toml` or via environmental variables.
|
||||
|
||||
#### LLM config groups must be subgroups of 'llm'
|
||||
|
||||
Prior to release 0.8, you can use arbitrary name for llm config in `config.toml`, e.g.
|
||||
|
||||
```toml
|
||||
[gpt-4o]
|
||||
model="gpt-4o"
|
||||
api_key="<your_api_key>"
|
||||
```
|
||||
|
||||
and then use `--llm-config` CLI argument to specify the desired LLM config group
|
||||
by name. This no longer works. Instead, the config group must be under `llm` group,
|
||||
e.g.:
|
||||
|
||||
```toml
|
||||
[llm.gpt-4o]
|
||||
model="gpt-4o"
|
||||
api_key="<your_api_key>"
|
||||
```
|
||||
|
||||
If you have a config group named `llm`, no need to change it, it will be used
|
||||
as the default LLM config group.
|
||||
|
||||
#### 'agent' group no longer contains 'name' field
|
||||
|
||||
Prior to release 0.8, you may or may not have a config group named `agent` that
|
||||
looks like this:
|
||||
|
||||
```toml
|
||||
[agent]
|
||||
name="CodeActAgent"
|
||||
memory_max_threads=2
|
||||
```
|
||||
|
||||
Note the `name` field is now removed. Instead, you should put `default_agent` field
|
||||
under `core` group, e.g.
|
||||
|
||||
```toml
|
||||
[core]
|
||||
# other configs
|
||||
default_agent='CodeActAgent'
|
||||
|
||||
[agent]
|
||||
llm_config='llm'
|
||||
memory_max_threads=2
|
||||
|
||||
[agent.CodeActAgent]
|
||||
llm_config='gpt-4o'
|
||||
```
|
||||
|
||||
Note that similar to `llm` subgroups, you can also define `agent` subgroups.
|
||||
Moreover, an agent can be associated with a specific LLM config group. For more
|
||||
detail, see the examples in `config.template.toml`.
|
||||
Generated
+4
-4
@@ -18,7 +18,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-use": "^17.5.1"
|
||||
"react-use": "^17.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
@@ -15264,9 +15264,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-use": {
|
||||
"version": "17.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz",
|
||||
"integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==",
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
|
||||
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
|
||||
"dependencies": {
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@xobotyi/scrollbar-width": "^1.9.5",
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-use": "^17.5.1"
|
||||
"react-use": "^17.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
|
||||
+19
-3
@@ -14,9 +14,20 @@ const sidebars: SidebarsConfig = {
|
||||
id: 'usage/getting-started',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Prompting Best Practices',
|
||||
id: 'usage/prompting-best-practices',
|
||||
type: 'category',
|
||||
label: 'Prompting',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Best Practices',
|
||||
id: 'usage/prompting-best-practices',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Micro-Agents',
|
||||
id: 'usage/micro-agents',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
@@ -110,6 +121,11 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Custom Sandbox',
|
||||
id: 'usage/how-to/custom-sandbox-guide',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Persist Session Data',
|
||||
id: 'usage/how-to/persist-session-data',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -128,6 +128,11 @@ def process_file(file_path):
|
||||
for error, count in error_counter.items()
|
||||
},
|
||||
},
|
||||
'costs': {
|
||||
'main_agent': sum(main_agent_cost),
|
||||
'editor': sum(editor_cost),
|
||||
'total': sum(main_agent_cost) + sum(editor_cost),
|
||||
},
|
||||
'statistics': {
|
||||
'avg_turns': sum(num_turns) / num_lines if num_lines > 0 else 0,
|
||||
'costs': {
|
||||
@@ -251,6 +256,7 @@ if __name__ == '__main__':
|
||||
print(
|
||||
f"Number of unfinished runs: {result['unfinished_runs']['count']} / {result['total_instances']} ({result['unfinished_runs']['percentage']:.2f}%)"
|
||||
)
|
||||
print(f"Total cost: {result['costs']['total']:.2f} USD")
|
||||
print('## Statistics')
|
||||
print(
|
||||
f"Avg. num of turns per instance: {result['statistics']['avg_turns']:.2f}"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import argparse
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def verify_instance_costs(row: pd.Series) -> float:
|
||||
"""
|
||||
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
|
||||
Also checks for duplicate consecutive costs which might indicate buggy counting.
|
||||
If the consecutive costs are identical, the file is affected by this bug:
|
||||
https://github.com/All-Hands-AI/OpenHands/issues/5383
|
||||
|
||||
Args:
|
||||
row: DataFrame row containing instance data with metrics
|
||||
Returns:
|
||||
float: The verified total cost for this instance (corrected if needed)
|
||||
"""
|
||||
try:
|
||||
metrics = row.get('metrics')
|
||||
if not metrics:
|
||||
logger.warning(f"Instance {row['instance_id']}: No metrics found")
|
||||
return 0.0
|
||||
|
||||
accumulated = metrics.get('accumulated_cost')
|
||||
costs = metrics.get('costs', [])
|
||||
|
||||
if accumulated is None:
|
||||
logger.warning(
|
||||
f"Instance {row['instance_id']}: No accumulated_cost in metrics"
|
||||
)
|
||||
return 0.0
|
||||
|
||||
# Check for duplicate consecutive costs and systematic even-odd pairs
|
||||
has_duplicate = False
|
||||
all_pairs_match = True
|
||||
|
||||
# Check each even-odd pair (0-1, 2-3, etc.)
|
||||
for i in range(0, len(costs) - 1, 2):
|
||||
if abs(costs[i]['cost'] - costs[i + 1]['cost']) < 1e-6:
|
||||
has_duplicate = True
|
||||
logger.debug(
|
||||
f"Instance {row['instance_id']}: Possible buggy double-counting detected! "
|
||||
f"Steps {i} and {i+1} have identical costs: {costs[i]['cost']:.2f}"
|
||||
)
|
||||
else:
|
||||
all_pairs_match = False
|
||||
break
|
||||
|
||||
# Calculate total cost, accounting for buggy double counting if detected
|
||||
if len(costs) >= 2 and has_duplicate and all_pairs_match:
|
||||
paired_steps_cost = sum(
|
||||
cost_entry['cost']
|
||||
for cost_entry in costs[: -1 if len(costs) % 2 else None]
|
||||
)
|
||||
real_paired_cost = paired_steps_cost / 2
|
||||
|
||||
unpaired_cost = costs[-1]['cost'] if len(costs) % 2 else 0
|
||||
total_cost = real_paired_cost + unpaired_cost
|
||||
|
||||
else:
|
||||
total_cost = sum(cost_entry['cost'] for cost_entry in costs)
|
||||
|
||||
if not abs(total_cost - accumulated) < 1e-6:
|
||||
logger.warning(
|
||||
f"Instance {row['instance_id']}: Cost mismatch: "
|
||||
f"accumulated: {accumulated:.2f}, sum of costs: {total_cost:.2f}, "
|
||||
)
|
||||
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error verifying costs for instance {row.get('instance_id', 'UNKNOWN')}: {e}"
|
||||
)
|
||||
return 0.0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Verify costs in SWE-bench output file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'input_filepath', type=str, help='Path to the output.jsonl file'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Load and verify the JSONL file
|
||||
df = pd.read_json(args.input_filepath, lines=True)
|
||||
logger.info(f'Loaded {len(df)} instances from {args.input_filepath}')
|
||||
|
||||
# Verify costs for each instance and sum up total
|
||||
total_cost = df.apply(verify_instance_costs, axis=1).sum()
|
||||
logger.info(f'Total verified cost across all instances: ${total_cost:.2f}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to process file: {e}')
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -33,7 +33,7 @@ if [ -d /workspace/$WORKSPACE_NAME ]; then
|
||||
rm -rf /workspace/$WORKSPACE_NAME
|
||||
fi
|
||||
mkdir -p /workspace
|
||||
ln -s /testbed /workspace/$WORKSPACE_NAME
|
||||
mv /testbed /workspace/$WORKSPACE_NAME
|
||||
|
||||
# Activate instance-specific environment
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
|
||||
@@ -11,6 +11,7 @@ describe("Browser", () => {
|
||||
browser: {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
updateCount: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -26,6 +27,7 @@ describe("Browser", () => {
|
||||
url: "https://example.com",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
updateCount: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,4 +70,12 @@ describe("ChatMessage", () => {
|
||||
);
|
||||
expect(screen.getByTestId("custom-component")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should apply correct styles to inline code", () => {
|
||||
render(<ChatMessage type="assistant" message="Here is some `inline code` text" />);
|
||||
const codeElement = screen.getByText("inline code");
|
||||
|
||||
expect(codeElement.tagName.toLowerCase()).toBe("code");
|
||||
expect(codeElement.closest("article")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
|
||||
|
||||
describe("ExpandableMessage", () => {
|
||||
it("should render with neutral border for non-action messages", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
|
||||
const element = screen.getByText("Hello");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with neutral border for error messages", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Error occurred" type="error" />);
|
||||
const element = screen.getByText("Error occurred");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with success icon for successful action messages", () => {
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
message="Command executed successfully"
|
||||
type="action"
|
||||
success={true}
|
||||
/>
|
||||
);
|
||||
const element = screen.getByText("Command executed successfully");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
const icon = screen.getByTestId("status-icon");
|
||||
expect(icon).toHaveClass("fill-success");
|
||||
});
|
||||
|
||||
it("should render with error icon for failed action messages", () => {
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
message="Command failed"
|
||||
type="action"
|
||||
success={false}
|
||||
/>
|
||||
);
|
||||
const element = screen.getByText("Command failed");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
const icon = screen.getByTestId("status-icon");
|
||||
expect(icon).toHaveClass("fill-danger");
|
||||
});
|
||||
|
||||
it("should render with neutral border and no icon for action messages without success prop", () => {
|
||||
renderWithProviders(<ExpandableMessage message="Running command" type="action" />);
|
||||
const element = screen.getByText("Running command");
|
||||
const container = element.closest("div.flex.gap-2.items-center.justify-between");
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { render, screen, within, fireEvent } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
|
||||
@@ -131,4 +131,60 @@ describe("InteractiveChatBox", () => {
|
||||
await user.click(stopButton);
|
||||
expect(onStopMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should handle image upload and message submission correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
const onStop = vi.fn();
|
||||
const onChange = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
onChange={onChange}
|
||||
value="test message"
|
||||
/>
|
||||
);
|
||||
|
||||
// Upload an image via the upload button - this should NOT clear the text input
|
||||
const file = new File(["dummy content"], "test.png", { type: "image/png" });
|
||||
const input = screen.getByTestId("upload-image-input");
|
||||
await user.upload(input, file);
|
||||
|
||||
// Verify text input was not cleared
|
||||
expect(screen.getByRole("textbox")).toHaveValue("test message");
|
||||
expect(onChange).not.toHaveBeenCalledWith("");
|
||||
|
||||
// Submit the message with image
|
||||
const submitButton = screen.getByRole("button", { name: "Send" });
|
||||
await user.click(submitButton);
|
||||
|
||||
// Verify onSubmit was called with the message and image
|
||||
expect(onSubmit).toHaveBeenCalledWith("test message", [file]);
|
||||
|
||||
// Verify onChange was called to clear the text input
|
||||
expect(onChange).toHaveBeenCalledWith("");
|
||||
|
||||
// Simulate parent component updating the value prop
|
||||
rerender(
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
onChange={onChange}
|
||||
value=""
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify the text input was cleared
|
||||
expect(screen.getByRole("textbox")).toHaveValue("");
|
||||
|
||||
// Upload another image - this should NOT clear the text input
|
||||
onChange.mockClear();
|
||||
await user.upload(input, file);
|
||||
|
||||
// Verify text input is still empty and onChange was not called
|
||||
expect(screen.getByRole("textbox")).toHaveValue("");
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.0",
|
||||
"version": "0.15.2",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
@@ -51,7 +51,7 @@
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@react-router/dev": "^7.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.60.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.1",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
@@ -5503,9 +5503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query": {
|
||||
"version": "5.61.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.61.4.tgz",
|
||||
"integrity": "sha512-QVVsY8hwrX9r6c8lLV48oY682SU2GeVlo0hWMSaOKkI05Yi4bXhw5jv7E2qkbjGrgA6DcVl3o/F0dT4wpT+/SQ==",
|
||||
"version": "5.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.1.tgz",
|
||||
"integrity": "sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.15.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -78,7 +78,7 @@
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@react-router/dev": "^7.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.60.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.1",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export function AppView() {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<iframe
|
||||
src="http://localhost:4000"
|
||||
className="h-full w-full border-0"
|
||||
title="App"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -83,9 +83,13 @@ export function ChatInput({
|
||||
};
|
||||
|
||||
const handleSubmitMessage = () => {
|
||||
if (textareaRef.current?.value) {
|
||||
onSubmit(textareaRef.current.value);
|
||||
textareaRef.current.value = "";
|
||||
const message = value || textareaRef.current?.value || "";
|
||||
if (message) {
|
||||
onSubmit(message);
|
||||
onChange?.("");
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,17 +6,21 @@ import { code } from "../markdown/code";
|
||||
import { ol, ul } from "../markdown/list";
|
||||
import ArrowUp from "#/icons/angle-up-solid.svg?react";
|
||||
import ArrowDown from "#/icons/angle-down-solid.svg?react";
|
||||
import CheckCircle from "#/icons/check-circle-solid.svg?react";
|
||||
import XCircle from "#/icons/x-circle-solid.svg?react";
|
||||
|
||||
interface ExpandableMessageProps {
|
||||
id?: string;
|
||||
message: string;
|
||||
type: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export function ExpandableMessage({
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
success,
|
||||
}: ExpandableMessageProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(true);
|
||||
@@ -31,22 +35,14 @@ export function ExpandableMessage({
|
||||
}
|
||||
}, [id, message, i18n.language]);
|
||||
|
||||
const border = type === "error" ? "border-danger" : "border-neutral-300";
|
||||
const textColor = type === "error" ? "text-danger" : "text-neutral-300";
|
||||
let arrowClasses = "h-4 w-4 ml-2 inline";
|
||||
if (type === "error") {
|
||||
arrowClasses += " fill-danger";
|
||||
} else {
|
||||
arrowClasses += " fill-neutral-300";
|
||||
}
|
||||
const arrowClasses = "h-4 w-4 ml-2 inline fill-neutral-300";
|
||||
const statusIconClasses = "h-4 w-4 ml-2 inline";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 ${border}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-between border-l-2 border-neutral-300 pl-2 my-2 py-2">
|
||||
<div className="text-sm leading-4 flex flex-col gap-2 max-w-full">
|
||||
{headline && (
|
||||
<p className={`${textColor} font-bold`}>
|
||||
<p className="text-neutral-300 font-bold">
|
||||
{headline}
|
||||
<button
|
||||
type="button"
|
||||
@@ -75,6 +71,21 @@ export function ExpandableMessage({
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
{type === "action" && success !== undefined && (
|
||||
<div className="flex-shrink-0">
|
||||
{success ? (
|
||||
<CheckCircle
|
||||
data-testid="status-icon"
|
||||
className={`${statusIconClasses} fill-success`}
|
||||
/>
|
||||
) : (
|
||||
<XCircle
|
||||
data-testid="status-icon"
|
||||
className={`${statusIconClasses} fill-danger`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ export function InteractiveChatBox({
|
||||
const handleSubmit = (message: string) => {
|
||||
onSubmit(message, images);
|
||||
setImages([]);
|
||||
if (message) {
|
||||
onChange?.("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ export function Messages({
|
||||
type={message.type}
|
||||
id={message.translationID}
|
||||
message={message.content}
|
||||
success={message.success}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AgentState from "#/types/agent-state";
|
||||
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
|
||||
@@ -14,6 +14,7 @@ import { Dropzone } from "./dropzone";
|
||||
import { FileExplorerHeader } from "./file-explorer-header";
|
||||
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
|
||||
import { OpenVSCodeButton } from "#/components/shared/buttons/open-vscode-button";
|
||||
import { addAssistantMessage } from "#/state/chat-slice";
|
||||
|
||||
interface FileExplorerProps {
|
||||
isOpen: boolean;
|
||||
@@ -22,15 +23,37 @@ interface FileExplorerProps {
|
||||
|
||||
export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const agentIsReady =
|
||||
curAgentState !== AgentState.INIT && curAgentState !== AgentState.LOADING;
|
||||
|
||||
const { data: paths, refetch, error } = useListFiles();
|
||||
const { mutate: uploadFiles } = useUploadFiles();
|
||||
const { refetch: getVSCodeUrl } = useVSCodeUrl();
|
||||
const { data: vscodeUrl } = useVSCodeUrl({ enabled: agentIsReady });
|
||||
|
||||
const handleOpenVSCode = () => {
|
||||
if (vscodeUrl?.vscode_url) {
|
||||
dispatch(
|
||||
addAssistantMessage(
|
||||
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
|
||||
),
|
||||
);
|
||||
window.open(vscodeUrl.vscode_url, "_blank");
|
||||
} else if (vscodeUrl?.error) {
|
||||
toast.error(
|
||||
`open-vscode-error-${new Date().getTime()}`,
|
||||
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
|
||||
error: vscodeUrl.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const selectFileInput = () => {
|
||||
fileInputRef.current?.click(); // Trigger the file browser
|
||||
@@ -142,11 +165,8 @@ export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
)}
|
||||
{isOpen && (
|
||||
<OpenVSCodeButton
|
||||
onClick={getVSCodeUrl}
|
||||
isDisabled={
|
||||
curAgentState === AgentState.INIT ||
|
||||
curAgentState === AgentState.LOADING
|
||||
}
|
||||
onClick={handleOpenVSCode}
|
||||
isDisabled={!agentIsReady}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { useFiles } from "#/context/files";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useListFiles } from "#/hooks/query/use-list-files";
|
||||
import { useListFile } from "#/hooks/query/use-list-file";
|
||||
import { Filename } from "./filename";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface TreeNodeProps {
|
||||
path: string;
|
||||
@@ -20,6 +22,7 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
|
||||
selectedPath,
|
||||
} = useFiles();
|
||||
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const isDirectory = path.endsWith("/");
|
||||
|
||||
@@ -39,6 +42,12 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
|
||||
}
|
||||
}, [fileContent, path]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedPath === path && !isDirectory) {
|
||||
refetch();
|
||||
}
|
||||
}, [curAgentState, selectedPath, path, isDirectory]);
|
||||
|
||||
const fileParts = path.split("/");
|
||||
const filename =
|
||||
fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2];
|
||||
|
||||
@@ -17,7 +17,20 @@ export function code({
|
||||
const match = /language-(\w+)/.exec(className || ""); // get the language
|
||||
|
||||
if (!match) {
|
||||
return <code className={className}>{children}</code>;
|
||||
return (
|
||||
<code
|
||||
className={className}
|
||||
style={{
|
||||
backgroundColor: "#2a3038",
|
||||
padding: "0.2em 0.4em",
|
||||
borderRadius: "4px",
|
||||
color: "#e6edf3",
|
||||
border: "1px solid #30363d",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import posthog from "posthog-js";
|
||||
import EllipsisH from "#/icons/ellipsis-h.svg?react";
|
||||
import { addUserMessage } from "#/state/chat-slice";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
|
||||
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
|
||||
@@ -28,7 +26,6 @@ export function ProjectMenuCard({
|
||||
githubData,
|
||||
}: ProjectMenuCardProps) {
|
||||
const { send } = useWsClient();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
|
||||
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
|
||||
@@ -56,7 +53,6 @@ Please push the changes to GitHub and open a pull request.
|
||||
);
|
||||
|
||||
send(event); // send to socket
|
||||
dispatch(addUserMessage(rawEvent)); // display in chat interface
|
||||
setContextMenuIsOpen(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -54,13 +54,13 @@ export function Sidebar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="px-1 flex flex-col gap-1">
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
|
||||
<div className="w-[34px] h-[34px] flex items-center justify-center">
|
||||
{user.isLoading && <LoadingSpinner size="small" />}
|
||||
{!user.isLoading && <AllHandsLogoButton onClick={handleClickLogo} />}
|
||||
</div>
|
||||
|
||||
<nav className="py-[18px] flex flex-col items-center gap-[18px]">
|
||||
<nav className="md:py-[18px] flex flex-row md:flex-col items-center gap-[18px]">
|
||||
<UserActions
|
||||
user={user.data ? { avatar_url: user.data.avatar_url } : undefined}
|
||||
onLogout={logout}
|
||||
|
||||
@@ -5,12 +5,10 @@ import { NavTab } from "./nav-tab";
|
||||
interface ContainerProps {
|
||||
label?: string;
|
||||
labels?: {
|
||||
id: string;
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
to: string;
|
||||
icon?: React.ReactNode;
|
||||
isBeta?: boolean;
|
||||
isActive: boolean;
|
||||
onClick: (id: string) => void;
|
||||
}[];
|
||||
children: React.ReactNode;
|
||||
className?: React.HTMLAttributes<HTMLDivElement>["className"];
|
||||
@@ -31,8 +29,8 @@ export function Container({
|
||||
>
|
||||
{labels && (
|
||||
<div className="flex text-xs h-[36px]">
|
||||
{labels.map(({ id, label: l, icon, isBeta, isActive, onClick }) => (
|
||||
<NavTab key={id} id={id} label={l} icon={icon} isBeta={isBeta} isActive={isActive} onClick={onClick} />
|
||||
{labels.map(({ label: l, to, icon, isBeta }) => (
|
||||
<NavTab key={to} to={to} label={l} icon={icon} isBeta={isBeta} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -41,7 +39,7 @@ export function Container({
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-scroll h-full rounded-b-xl">{children}</div>
|
||||
<div className="overflow-hidden h-full rounded-b-xl">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function CountBadge({ count }: { count: number }) {
|
||||
return (
|
||||
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
|
||||
{count}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,32 @@
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BetaBadge } from "./beta-badge";
|
||||
|
||||
interface NavTabProps {
|
||||
id: string;
|
||||
label: string;
|
||||
to: string;
|
||||
label: string | React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
isBeta?: boolean;
|
||||
isActive: boolean;
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export function NavTab({ id, label, icon, isBeta, isActive, onClick }: NavTabProps) {
|
||||
export function NavTab({ to, label, icon, isBeta }: NavTabProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick(id)}
|
||||
className={cn(
|
||||
"px-2 border-b border-r border-neutral-600 bg-root-primary flex-1",
|
||||
"first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0",
|
||||
"flex items-center gap-2",
|
||||
isActive && "bg-root-secondary",
|
||||
)}
|
||||
<NavLink
|
||||
end
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"px-2 border-b border-r border-neutral-600 bg-root-primary flex-1",
|
||||
"first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0",
|
||||
"flex items-center gap-2",
|
||||
isActive && "bg-root-secondary",
|
||||
)
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
{isBeta && <BetaBadge />}
|
||||
</button>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from "#/utils/toast";
|
||||
import { addAssistantMessage } from "#/state/chat-slice";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useVSCodeUrl = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
export const useVSCodeUrl = (config: { enabled: boolean }) => {
|
||||
const data = useQuery({
|
||||
queryKey: ["vscode_url"],
|
||||
queryFn: OpenHands.getVSCodeUrl,
|
||||
enabled: false,
|
||||
enabled: config.enabled,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const { data: vscodeUrlObject, isFetching } = data;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFetching) return;
|
||||
|
||||
if (vscodeUrlObject?.vscode_url) {
|
||||
dispatch(
|
||||
addAssistantMessage(
|
||||
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
|
||||
),
|
||||
);
|
||||
window.open(vscodeUrlObject.vscode_url, "_blank");
|
||||
} else if (vscodeUrlObject?.error) {
|
||||
toast.error(
|
||||
`open-vscode-error-${new Date().getTime()}`,
|
||||
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
|
||||
error: vscodeUrlObject.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [vscodeUrlObject, isFetching]);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -2017,6 +2017,9 @@
|
||||
"ACTION_MESSAGE$WRITE": {
|
||||
"en": "Writing to a file"
|
||||
},
|
||||
"ACTION_MESSAGE$BROWSE": {
|
||||
"en": "Browsing the web"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RUN": {
|
||||
"en": "Ran a bash command"
|
||||
},
|
||||
@@ -2029,6 +2032,9 @@
|
||||
"OBSERVATION_MESSAGE$WRITE": {
|
||||
"en": "Wrote to a file"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$BROWSE": {
|
||||
"en": "Browsing completed"
|
||||
},
|
||||
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
|
||||
"en": "Show details"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
+22
-3
@@ -27,18 +27,37 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
.markdown-body :not(pre) > code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
background-color: var(--bg-neutral-muted);
|
||||
background-color: #2a3038;
|
||||
border-radius: 4px;
|
||||
color: #e6edf3;
|
||||
border: 1px solid #30363d;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #2a3038;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
border: 1px solid #30363d;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: pre;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: block;
|
||||
color: #e6edf3;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@ type Message = {
|
||||
timestamp: string;
|
||||
imageUrls?: string[];
|
||||
type?: "thought" | "error" | "action";
|
||||
success?: boolean;
|
||||
pending?: boolean;
|
||||
translationID?: string;
|
||||
eventID?: number;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { AppView } from "#/components/features/app/app-view";
|
||||
|
||||
function App() {
|
||||
return <AppView />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -52,6 +52,7 @@ export const useWSStatusChange = () => {
|
||||
|
||||
if (gitHubToken && selectedRepository) {
|
||||
dispatch(clearSelectedRepository());
|
||||
additionalInfo = `Repository ${selectedRepository} has been cloned to /workspace. Please check the /workspace for files.`;
|
||||
} else if (importedProjectZip) {
|
||||
// if there's an uploaded project zip, add it to the chat
|
||||
additionalInfo =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Controls } from "#/components/features/controls/controls";
|
||||
import { RootState } from "#/store";
|
||||
@@ -20,16 +21,11 @@ import { useUserPrefs } from "#/context/user-prefs-context";
|
||||
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
|
||||
import { Container } from "#/components/layout/container";
|
||||
import Security from "#/components/shared/modals/security/security";
|
||||
import { useState } from "react";
|
||||
import { AppView } from "#/components/features/app/app-view";
|
||||
import { Workspace } from "#/components/features/workspace/workspace";
|
||||
import { JupyterView } from "#/components/features/jupyter/jupyter-view";
|
||||
import { BrowserView } from "#/components/features/browser/browser-view";
|
||||
import { CountBadge } from "#/components/layout/count-badge";
|
||||
|
||||
function App() {
|
||||
const { token, gitHubToken } = useAuth();
|
||||
const { settings } = useUserPrefs();
|
||||
const [activeTab, setActiveTab] = useState("workspace");
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useConversationConfig();
|
||||
@@ -38,6 +34,8 @@ function App() {
|
||||
(state: RootState) => state.initalQuery,
|
||||
);
|
||||
|
||||
const { updateCount } = useSelector((state: RootState) => state.browser);
|
||||
|
||||
const { data: latestGitHubCommit } = useLatestRepoCommit({
|
||||
repository: selectedRepository,
|
||||
});
|
||||
@@ -64,25 +62,6 @@ function App() {
|
||||
onOpenChange: onSecurityModalOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case "workspace":
|
||||
return (
|
||||
<FilesProvider>
|
||||
<Workspace />
|
||||
</FilesProvider>
|
||||
);
|
||||
case "jupyter":
|
||||
return <JupyterView />;
|
||||
case "browser":
|
||||
return <BrowserView />;
|
||||
case "app":
|
||||
return <AppView />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<WsClientProvider
|
||||
enabled
|
||||
@@ -94,46 +73,31 @@ function App() {
|
||||
<EventHandler>
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto gap-3">
|
||||
<Container className="w-[390px] max-h-full relative">
|
||||
<Container className="w-full md:w-[390px] max-h-full relative">
|
||||
<ChatInterface />
|
||||
</Container>
|
||||
|
||||
<div className="flex flex-col grow gap-3">
|
||||
<div className="hidden md:flex flex-col grow gap-3">
|
||||
<Container
|
||||
className="h-2/3"
|
||||
labels={[
|
||||
{
|
||||
id: "workspace",
|
||||
label: "Workspace",
|
||||
icon: <CodeIcon />,
|
||||
isActive: activeTab === "workspace",
|
||||
onClick: setActiveTab
|
||||
},
|
||||
{
|
||||
id: "jupyter",
|
||||
label: "Jupyter",
|
||||
icon: <ListIcon />,
|
||||
isActive: activeTab === "jupyter",
|
||||
onClick: setActiveTab
|
||||
},
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
id: "browser",
|
||||
label: "Browser",
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
Browser
|
||||
{updateCount > 0 && <CountBadge count={updateCount} />}
|
||||
</div>
|
||||
),
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
isBeta: true,
|
||||
isActive: activeTab === "browser",
|
||||
onClick: setActiveTab
|
||||
},
|
||||
{
|
||||
id: "app",
|
||||
label: "App",
|
||||
icon: <GlobeIcon />,
|
||||
isActive: activeTab === "app",
|
||||
onClick: setActiveTab
|
||||
},
|
||||
]}
|
||||
>
|
||||
{renderActiveTab()}
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
|
||||
@@ -79,11 +79,11 @@ export default function MainApp() {
|
||||
return (
|
||||
<div
|
||||
data-testid="root-layout"
|
||||
className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3"
|
||||
className="bg-root-primary p-3 h-screen md:min-w-[1024px] overflow-x-hidden flex flex-col md:flex-row gap-3"
|
||||
>
|
||||
<Sidebar />
|
||||
|
||||
<div className="h-full w-full relative">
|
||||
<div className="h-[calc(100%-50px)] md:h-full w-full relative">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,21 +15,16 @@ import {
|
||||
ObservationMessage,
|
||||
StatusMessage,
|
||||
} from "#/types/message";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
|
||||
const messageActions = {
|
||||
[ActionType.BROWSE]: (message: ActionMessage) => {
|
||||
if (message.args.thought) {
|
||||
store.dispatch(addAssistantMessage(message.args.thought));
|
||||
} else {
|
||||
if (!message.args.thought && message.message) {
|
||||
store.dispatch(addAssistantMessage(message.message));
|
||||
}
|
||||
},
|
||||
[ActionType.BROWSE_INTERACTIVE]: (message: ActionMessage) => {
|
||||
if (message.args.thought) {
|
||||
store.dispatch(addAssistantMessage(message.args.thought));
|
||||
} else {
|
||||
if (!message.args.thought && message.message) {
|
||||
store.dispatch(addAssistantMessage(message.message));
|
||||
}
|
||||
},
|
||||
@@ -107,6 +102,10 @@ export function handleAssistantMessage(message: Record<string, unknown>) {
|
||||
} else if (message.status_update) {
|
||||
handleStatusMessage(message as unknown as StatusMessage);
|
||||
} else {
|
||||
EventLogger.error(`Unknown message type ${message}`);
|
||||
store.dispatch(
|
||||
addErrorMessage({
|
||||
message: "Unknown message type received",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const saveSettings = (settings: Partial<Settings>) => {
|
||||
if (!isValid) return;
|
||||
let value = settings[key as keyof Settings];
|
||||
if (value === undefined || value === null) value = "";
|
||||
localStorage.setItem(key, value.toString());
|
||||
localStorage.setItem(key, value.toString().trim());
|
||||
});
|
||||
localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString());
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ export const initialState = {
|
||||
url: "https://github.com/All-Hands-AI/OpenHands",
|
||||
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
|
||||
screenshotSrc: "",
|
||||
// Counter for browser updates
|
||||
updateCount: 0,
|
||||
};
|
||||
|
||||
export const browserSlice = createSlice({
|
||||
@@ -16,6 +18,7 @@ export const browserSlice = createSlice({
|
||||
},
|
||||
setScreenshotSrc: (state, action) => {
|
||||
state.screenshotSrc = action.payload;
|
||||
state.updateCount += 1;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import {
|
||||
OpenHandsObservation,
|
||||
CommandObservation,
|
||||
IPythonObservation,
|
||||
} from "#/types/core/observations";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsEventType } from "#/types/core/base";
|
||||
|
||||
type SliceState = { messages: Message[] };
|
||||
|
||||
const MAX_CONTENT_LENGTH = 1000;
|
||||
|
||||
const HANDLED_ACTIONS = ["run", "run_ipython", "write", "read"];
|
||||
const HANDLED_ACTIONS: OpenHandsEventType[] = [
|
||||
"run",
|
||||
"run_ipython",
|
||||
"write",
|
||||
"read",
|
||||
"browse",
|
||||
];
|
||||
|
||||
function getRiskText(risk: ActionSecurityRisk) {
|
||||
switch (risk) {
|
||||
@@ -92,6 +103,8 @@ export const chatSlice = createSlice({
|
||||
text = `${action.payload.args.path}\n${content}`;
|
||||
} else if (actionID === "read") {
|
||||
text = action.payload.args.path;
|
||||
} else if (actionID === "browse") {
|
||||
text = `Browsing ${action.payload.args.url}`;
|
||||
}
|
||||
if (actionID === "run" || actionID === "run_ipython") {
|
||||
if (
|
||||
@@ -129,6 +142,18 @@ export const chatSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
causeMessage.translationID = translationID;
|
||||
// Set success property based on observation type
|
||||
if (observationID === "run") {
|
||||
const commandObs = observation.payload as CommandObservation;
|
||||
causeMessage.success = commandObs.extras.exit_code === 0;
|
||||
} else if (observationID === "run_ipython") {
|
||||
// For IPython, we consider it successful if there's no error message
|
||||
const ipythonObs = observation.payload as IPythonObservation;
|
||||
causeMessage.success = !ipythonObs.message
|
||||
.toLowerCase()
|
||||
.includes("error");
|
||||
}
|
||||
|
||||
if (observationID === "run" || observationID === "run_ipython") {
|
||||
let { content } = observation.payload;
|
||||
if (content.length > MAX_CONTENT_LENGTH) {
|
||||
@@ -136,6 +161,16 @@ export const chatSlice = createSlice({
|
||||
}
|
||||
content = `\`\`\`\n${content}\n\`\`\``;
|
||||
causeMessage.content = content; // Observation content includes the action
|
||||
} else if (observationID === "browse") {
|
||||
let content = `**URL:** ${observation.payload.extras.url}\n`;
|
||||
if (observation.payload.extras.error) {
|
||||
content += `**Error:**\n${observation.payload.extras.error}\n`;
|
||||
}
|
||||
content += `**Output:**\n${observation.payload.content}`;
|
||||
if (content.length > MAX_CONTENT_LENGTH) {
|
||||
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
|
||||
}
|
||||
causeMessage.content = content;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -52,6 +52,21 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WriteObservation extends OpenHandsObservationEvent<"write"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReadObservation extends OpenHandsObservationEvent<"read"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
|
||||
source: "user";
|
||||
extras: {
|
||||
@@ -65,4 +80,6 @@ export type OpenHandsObservation =
|
||||
| IPythonObservation
|
||||
| DelegateObservation
|
||||
| BrowseObservation
|
||||
| WriteObservation
|
||||
| ReadObservation
|
||||
| ErrorObservation;
|
||||
|
||||
@@ -3,22 +3,19 @@ enum TabOption {
|
||||
CODE = "code",
|
||||
BROWSER = "browser",
|
||||
JUPYTER = "jupyter",
|
||||
APP = "app",
|
||||
}
|
||||
|
||||
type TabType =
|
||||
| TabOption.PLANNER
|
||||
| TabOption.CODE
|
||||
| TabOption.BROWSER
|
||||
| TabOption.JUPYTER
|
||||
| TabOption.APP;
|
||||
| TabOption.JUPYTER;
|
||||
|
||||
const AllTabs = [
|
||||
TabOption.CODE,
|
||||
TabOption.BROWSER,
|
||||
TabOption.PLANNER,
|
||||
TabOption.JUPYTER,
|
||||
TabOption.APP,
|
||||
];
|
||||
|
||||
export { AllTabs, TabOption, type TabType };
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
'root-secondary': '#262626',
|
||||
'hyperlink': '#007AFF',
|
||||
'danger': '#EF3744',
|
||||
'success': '#4CAF50',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+24
-1
@@ -6,10 +6,31 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { RenderOptions, render } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { UserPrefsProvider } from "#/context/user-prefs-context";
|
||||
|
||||
// Initialize i18n for tests
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
ns: ["translation"],
|
||||
defaultNS: "translation",
|
||||
resources: {
|
||||
en: {
|
||||
translation: {},
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
const setupStore = (preloadedState?: Partial<RootState>): AppStore =>
|
||||
configureStore({
|
||||
reducer: rootReducer,
|
||||
@@ -40,7 +61,9 @@ export function renderWithProviders(
|
||||
<UserPrefsProvider>
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{children}
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</UserPrefsProvider>
|
||||
|
||||
@@ -12,7 +12,13 @@ HTMLElement.prototype.scrollTo = vi.fn();
|
||||
// Mock the i18n provider
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: {
|
||||
language: "en",
|
||||
exists: () => false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock requests during tests
|
||||
|
||||
@@ -110,4 +110,4 @@ The agent is implemented in two main files:
|
||||
2. `function_calling.py`: Tool definitions and function calling interface with:
|
||||
- Tool parameter specifications
|
||||
- Tool descriptions and examples
|
||||
- Function calling response parsing
|
||||
- Function calling response parsing
|
||||
|
||||
@@ -5,7 +5,7 @@ import traceback
|
||||
from typing import Callable, ClassVar, Type
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import ContextWindowExceededError
|
||||
from litellm.exceptions import BadRequestError, ContextWindowExceededError
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State, TrafficControlState
|
||||
@@ -184,7 +184,7 @@ class AgentController:
|
||||
self.state.local_iteration += 1
|
||||
|
||||
async def update_state_after_step(self):
|
||||
# update metrics especially for cost. Use deepcopy to avoid it being modified by agent.reset()
|
||||
# update metrics especially for cost. Use deepcopy to avoid it being modified by agent._reset()
|
||||
self.state.local_metrics = copy.deepcopy(self.agent.llm.metrics)
|
||||
|
||||
async def _react_to_exception(
|
||||
@@ -317,9 +317,10 @@ class AgentController:
|
||||
elif action.source == EventSource.AGENT and action.wait_for_response:
|
||||
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
|
||||
|
||||
def reset_task(self) -> None:
|
||||
"""Resets the agent's task."""
|
||||
def _reset(self) -> None:
|
||||
"""Resets the agent controller"""
|
||||
self.almost_stuck = 0
|
||||
self._pending_action = None
|
||||
self.agent.reset()
|
||||
|
||||
async def set_agent_state_to(self, new_state: AgentState) -> None:
|
||||
@@ -337,7 +338,7 @@ class AgentController:
|
||||
return
|
||||
|
||||
if new_state in (AgentState.STOPPED, AgentState.ERROR):
|
||||
self.reset_task()
|
||||
self._reset()
|
||||
elif (
|
||||
new_state == AgentState.RUNNING
|
||||
and self.state.agent_state == AgentState.PAUSED
|
||||
@@ -454,13 +455,10 @@ class AgentController:
|
||||
await asyncio.sleep(1)
|
||||
return
|
||||
|
||||
if self._is_stuck():
|
||||
await self._react_to_exception(RuntimeError('Agent got stuck in a loop'))
|
||||
return
|
||||
|
||||
if self.delegate is not None:
|
||||
assert self.delegate != self
|
||||
if self.delegate.get_agent_state() == AgentState.PAUSED:
|
||||
# no need to check too often
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
await self._delegate_step()
|
||||
@@ -487,6 +485,10 @@ class AgentController:
|
||||
if stop_step:
|
||||
return
|
||||
|
||||
if self._is_stuck():
|
||||
await self._react_to_exception(RuntimeError('Agent got stuck in a loop'))
|
||||
return
|
||||
|
||||
self.update_state_before_step()
|
||||
action: Action = NullAction()
|
||||
try:
|
||||
@@ -507,15 +509,24 @@ class AgentController:
|
||||
EventSource.AGENT,
|
||||
)
|
||||
return
|
||||
except ContextWindowExceededError:
|
||||
# When context window is exceeded, keep roughly half of agent interactions
|
||||
self.state.history = self._apply_conversation_window(self.state.history)
|
||||
except (ContextWindowExceededError, BadRequestError) as e:
|
||||
# FIXME: this is a hack until a litellm fix is confirmed
|
||||
# Check if this is a nested context window error
|
||||
error_str = str(e).lower()
|
||||
if (
|
||||
'contextwindowexceedederror' in error_str
|
||||
or 'prompt is too long' in error_str
|
||||
or isinstance(e, ContextWindowExceededError)
|
||||
):
|
||||
# When context window is exceeded, keep roughly half of agent interactions
|
||||
self.state.history = self._apply_conversation_window(self.state.history)
|
||||
|
||||
# Save the ID of the first event in our truncated history for future reloading
|
||||
if self.state.history:
|
||||
self.state.start_id = self.state.history[0].id
|
||||
# Don't add error event - let the agent retry with reduced context
|
||||
return
|
||||
# Save the ID of the first event in our truncated history for future reloading
|
||||
if self.state.history:
|
||||
self.state.start_id = self.state.history[0].id
|
||||
# Don't add error event - let the agent retry with reduced context
|
||||
return
|
||||
raise
|
||||
|
||||
if action.runnable:
|
||||
if self.state.confirmation_mode and (
|
||||
|
||||
@@ -48,6 +48,7 @@ class SandboxConfig:
|
||||
False # once enabled, OpenHands would lint files after editing
|
||||
)
|
||||
use_host_network: bool = False
|
||||
runtime_extra_build_args: list[str] | None = None
|
||||
initialize_plugins: bool = True
|
||||
force_rebuild_runtime: bool = False
|
||||
runtime_extra_deps: str | None = None
|
||||
|
||||
+16
-14
@@ -5,7 +5,8 @@ import re
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Literal, Mapping
|
||||
from types import TracebackType
|
||||
from typing import Any, Literal, Mapping
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
@@ -61,7 +62,8 @@ class NoColorFormatter(logging.Formatter):
|
||||
|
||||
|
||||
def strip_ansi(s: str) -> str:
|
||||
"""
|
||||
"""Remove ANSI escape sequences (terminal color/formatting codes) from string.
|
||||
|
||||
Removes ANSI escape sequences from str, as defined by ECMA-048 in
|
||||
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf
|
||||
# https://github.com/ewen-lbh/python-strip-ansi/blob/master/strip_ansi/__init__.py
|
||||
@@ -136,6 +138,7 @@ class RollingLogger:
|
||||
|
||||
def print_lines(self):
|
||||
"""Display the last n log_lines in the console (not for file logging).
|
||||
|
||||
This will create the effect of a rolling display in the console.
|
||||
"""
|
||||
self.move_back()
|
||||
@@ -143,18 +146,14 @@ class RollingLogger:
|
||||
self.replace_current_line(line)
|
||||
|
||||
def move_back(self, amount=-1):
|
||||
"""
|
||||
'\033[F' moves the cursor up one line.
|
||||
"""
|
||||
r"""'\033[F' moves the cursor up one line."""
|
||||
if amount == -1:
|
||||
amount = self.max_lines
|
||||
self._write('\033[F' * (self.max_lines))
|
||||
self._flush()
|
||||
|
||||
def replace_current_line(self, line=''):
|
||||
"""
|
||||
'\033[2K\r' clears the line and moves the cursor to the beginning of the line.
|
||||
"""
|
||||
r"""'\033[2K\r' clears the line and moves the cursor to the beginning of the line."""
|
||||
self._write('\033[2K' + line + '\n')
|
||||
self._flush()
|
||||
|
||||
@@ -232,18 +231,21 @@ def get_file_handler(log_dir: str, log_level: int = logging.INFO):
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
|
||||
|
||||
def log_uncaught_exceptions(ex_cls, ex, tb):
|
||||
def log_uncaught_exceptions(
|
||||
ex_cls: type[BaseException], ex: BaseException, tb: TracebackType | None
|
||||
) -> Any:
|
||||
"""Logs uncaught exceptions along with the traceback.
|
||||
|
||||
Args:
|
||||
ex_cls (type): The type of the exception.
|
||||
ex (Exception): The exception instance.
|
||||
tb (traceback): The traceback object.
|
||||
ex_cls: The type of the exception.
|
||||
ex: The exception instance.
|
||||
tb: The traceback object.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
logging.error(''.join(traceback.format_tb(tb)))
|
||||
if tb: # Add check since tb can be None
|
||||
logging.error(''.join(traceback.format_tb(tb)))
|
||||
logging.error('{0}: {1}'.format(ex_cls, ex))
|
||||
|
||||
|
||||
@@ -283,7 +285,7 @@ logging.getLogger('LiteLLM Proxy').disabled = True
|
||||
|
||||
|
||||
class LlmFileHandler(logging.FileHandler):
|
||||
"""# LLM prompt and response logging"""
|
||||
"""LLM prompt and response logging."""
|
||||
|
||||
def __init__(self, filename, mode='a', encoding='utf-8', delay=False):
|
||||
"""Initializes an instance of LlmFileHandler.
|
||||
|
||||
@@ -105,14 +105,6 @@ class Message(BaseModel):
|
||||
|
||||
message_dict: dict = {'content': content, 'role': self.role}
|
||||
|
||||
# pop content if it's empty
|
||||
if not content or (
|
||||
len(content) == 1
|
||||
and content[0]['type'] == 'text'
|
||||
and content[0]['text'] == ''
|
||||
):
|
||||
message_dict.pop('content')
|
||||
|
||||
if role_tool_with_prompt_caching:
|
||||
message_dict['cache_control'] = {'type': 'ephemeral'}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ class CmdOutputObservation(Observation):
|
||||
def message(self) -> str:
|
||||
return f'Command `{self.command}` executed with exit code {self.exit_code}.'
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
return not self.error
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code})**\n{self.content}'
|
||||
|
||||
@@ -42,5 +46,9 @@ class IPythonRunCellObservation(Observation):
|
||||
def message(self) -> str:
|
||||
return 'Code executed in IPython cell.'
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
return True # IPython cells are always considered successful
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'**IPythonRunCellObservation**\n{self.content}'
|
||||
|
||||
@@ -83,6 +83,9 @@ def event_to_dict(event: 'Event') -> dict:
|
||||
elif 'observation' in d:
|
||||
d['content'] = props.pop('content', '')
|
||||
d['extras'] = props
|
||||
# Include success field for CmdOutputObservation
|
||||
if hasattr(event, 'success'):
|
||||
d['success'] = event.success
|
||||
else:
|
||||
raise ValueError('Event must be either action or observation')
|
||||
return d
|
||||
|
||||
@@ -50,4 +50,5 @@ def observation_from_dict(observation: dict) -> Observation:
|
||||
observation.pop('message', None)
|
||||
content = observation.pop('content', '')
|
||||
extras = observation.pop('extras', {})
|
||||
|
||||
return observation_class(content=content, **extras)
|
||||
|
||||
@@ -16,7 +16,7 @@ class DebugMixin:
|
||||
debug_message = MESSAGE_SEPARATOR.join(
|
||||
self._format_message_content(msg)
|
||||
for msg in messages
|
||||
if msg.get('content', None)
|
||||
if msg['content'] is not None
|
||||
)
|
||||
|
||||
if debug_message:
|
||||
|
||||
@@ -321,7 +321,7 @@ def convert_fncall_messages_to_non_fncall_messages(
|
||||
first_user_message_encountered = False
|
||||
for message in messages:
|
||||
role = message['role']
|
||||
content = message.get('content', '')
|
||||
content = message['content']
|
||||
|
||||
# 1. SYSTEM MESSAGES
|
||||
# append system prompt suffix to content
|
||||
|
||||
@@ -51,6 +51,7 @@ class LongTermMemory:
|
||||
self.embed_model = EmbeddingsLoader.get_embedding_model(
|
||||
embedding_strategy, llm_config
|
||||
)
|
||||
logger.debug(f'Using embedding model: {self.embed_model}')
|
||||
|
||||
# instantiate the index
|
||||
self.index = VectorStoreIndex.from_vector_store(vector_store, self.embed_model)
|
||||
|
||||
@@ -61,7 +61,7 @@ Follow these steps to use this workflow in your own repository:
|
||||
2. Create a draft PR if successful, or push a branch if unsuccessful
|
||||
3. Comment on the issue with the results
|
||||
|
||||
Need help? Feel free to [open an issue](https://github.com/all-hands-ai/openhands-resolver/issues) or email us at [contact@all-hands.dev](mailto:contact@all-hands.dev).
|
||||
Need help? Feel free to [open an issue](https://github.com/all-hands-ai/openhands/issues) or email us at [contact@all-hands.dev](mailto:contact@all-hands.dev).
|
||||
|
||||
## Manual Installation
|
||||
|
||||
@@ -111,7 +111,7 @@ python -m openhands.resolver.resolve_issue --repo [OWNER]/[REPO] --issue-number
|
||||
For instance, if you want to resolve issue #100 in this repo, you would run:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_issue --repo all-hands-ai/openhands-resolver --issue-number 100
|
||||
python -m openhands.resolver.resolve_issue --repo all-hands-ai/openhands --issue-number 100
|
||||
```
|
||||
|
||||
The output will be written to the `output/` directory.
|
||||
@@ -119,7 +119,7 @@ The output will be written to the `output/` directory.
|
||||
If you've installed the package from source using poetry, you can use:
|
||||
|
||||
```bash
|
||||
poetry run python openhands/resolver/resolve_issue.py --repo all-hands-ai/openhands-resolver --issue-number 100
|
||||
poetry run python openhands/resolver/resolve_issue.py --repo all-hands-ai/openhands --issue-number 100
|
||||
```
|
||||
|
||||
For resolving multiple issues at once (e.g., in a batch process), you can use the `resolve_all_issues` command:
|
||||
@@ -131,7 +131,7 @@ python -m openhands.resolver.resolve_all_issues --repo [OWNER]/[REPO] --issue-nu
|
||||
For example:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_all_issues --repo all-hands-ai/openhands-resolver --issue-numbers 100,101,102
|
||||
python -m openhands.resolver.resolve_all_issues --repo all-hands-ai/openhands --issue-numbers 100,101,102
|
||||
```
|
||||
|
||||
## Responding to PR Comments
|
||||
|
||||
@@ -23,10 +23,10 @@ jobs:
|
||||
with:
|
||||
macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }}
|
||||
max_iterations: ${{ fromJson(vars.OPENHANDS_MAX_ITER || 50) }}
|
||||
base_container_image: ${{ vars.OPENHANDS_BASE_CONTAINER_IMAGE || "" }}
|
||||
base_container_image: ${{ vars.OPENHANDS_BASE_CONTAINER_IMAGE || '' }}
|
||||
LLM_MODEL: ${{ vars.LLM_MODEL }}
|
||||
secrets:
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
|
||||
LLM_MODEL: ${{ secrets.LLM_MODEL }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
|
||||
@@ -62,19 +62,23 @@ class IssueHandler(IssueHandlerInterface):
|
||||
params: dict[str, int | str] = {'state': 'open', 'per_page': 100, 'page': 1}
|
||||
all_issues = []
|
||||
|
||||
# Get issues, page by page
|
||||
while True:
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
issues = response.json()
|
||||
|
||||
# No more issues, break the loop
|
||||
if not issues:
|
||||
break
|
||||
|
||||
# Sanity check - the response is a list of dictionaries
|
||||
if not isinstance(issues, list) or any(
|
||||
[not isinstance(issue, dict) for issue in issues]
|
||||
):
|
||||
raise ValueError('Expected list of dictionaries from Github API.')
|
||||
|
||||
# Add the issues to the final list
|
||||
all_issues.extend(issues)
|
||||
assert isinstance(params['page'], int)
|
||||
params['page'] += 1
|
||||
@@ -107,7 +111,12 @@ class IssueHandler(IssueHandlerInterface):
|
||||
def _get_issue_comments(
|
||||
self, issue_number: int, comment_id: int | None = None
|
||||
) -> list[str] | None:
|
||||
"""Download comments for a specific issue from Github."""
|
||||
"""Retrieve comments for a specific issue from Github.
|
||||
|
||||
Args:
|
||||
issue_number: The ID of the issue to get comments for
|
||||
comment_id: The ID of a single comment, if provided, otherwise all comments
|
||||
"""
|
||||
url = f'https://api.github.com/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments'
|
||||
headers = {
|
||||
'Authorization': f'token {self.token}',
|
||||
@@ -116,6 +125,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
params = {'per_page': 100, 'page': 1}
|
||||
all_comments = []
|
||||
|
||||
# Get comments, page by page
|
||||
while True:
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
@@ -124,6 +134,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
if not comments:
|
||||
break
|
||||
|
||||
# If a single comment ID is provided, return only that comment
|
||||
if comment_id:
|
||||
matching_comment = next(
|
||||
(
|
||||
@@ -136,6 +147,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
if matching_comment:
|
||||
return [matching_comment]
|
||||
else:
|
||||
# Otherwise, return all comments
|
||||
all_comments.extend([comment['body'] for comment in comments])
|
||||
|
||||
params['page'] += 1
|
||||
@@ -147,6 +159,10 @@ class IssueHandler(IssueHandlerInterface):
|
||||
) -> list[GithubIssue]:
|
||||
"""Download issues from Github.
|
||||
|
||||
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 Github issues.
|
||||
"""
|
||||
@@ -203,7 +219,14 @@ class IssueHandler(IssueHandlerInterface):
|
||||
prompt_template: str,
|
||||
repo_instruction: str | None = None,
|
||||
) -> tuple[str, list[str]]:
|
||||
"""Generate instruction for the agent."""
|
||||
"""Generate instruction for the agent.
|
||||
|
||||
Args:
|
||||
issue: The issue to generate instruction for
|
||||
prompt_template: The prompt template to use
|
||||
repo_instruction: The repository instruction if it exists
|
||||
"""
|
||||
|
||||
# Format thread comments if they exist
|
||||
thread_context = ''
|
||||
if issue.thread_comments:
|
||||
@@ -211,6 +234,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
issue.thread_comments
|
||||
)
|
||||
|
||||
# Extract image URLs from the issue body and thread comments
|
||||
images = []
|
||||
images.extend(self._extract_image_urls(issue.body))
|
||||
images.extend(self._extract_image_urls(thread_context))
|
||||
@@ -227,8 +251,14 @@ class IssueHandler(IssueHandlerInterface):
|
||||
def guess_success(
|
||||
self, issue: GithubIssue, history: list[Event]
|
||||
) -> tuple[bool, None | list[bool], str]:
|
||||
"""Guess if the issue is fixed based on the history and the issue description."""
|
||||
"""Guess if the issue is fixed based on the history and the issue description.
|
||||
|
||||
Args:
|
||||
issue: The issue to check
|
||||
history: The agent's history
|
||||
"""
|
||||
last_message = history[-1].message
|
||||
|
||||
# Include thread comments in the prompt if they exist
|
||||
issue_context = issue.body
|
||||
if issue.thread_comments:
|
||||
@@ -236,6 +266,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
issue.thread_comments
|
||||
)
|
||||
|
||||
# Prepare the prompt
|
||||
with open(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
@@ -246,6 +277,7 @@ class IssueHandler(IssueHandlerInterface):
|
||||
template = jinja2.Template(f.read())
|
||||
prompt = template.render(issue_context=issue_context, last_message=last_message)
|
||||
|
||||
# Get the LLM response and check for 'success' and 'explanation' in the answer
|
||||
response = self.llm.completion(messages=[{'role': 'user', 'content': prompt}])
|
||||
|
||||
answer = response.choices[0].message.content.strip()
|
||||
@@ -328,6 +360,7 @@ class PRHandler(IssueHandler):
|
||||
|
||||
variables = {'owner': self.owner, 'repo': self.repo, 'pr': pull_number}
|
||||
|
||||
# Run the query
|
||||
url = 'https://api.github.com/graphql'
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
@@ -394,10 +427,12 @@ class PRHandler(IssueHandler):
|
||||
review_thread['body'] + '\n'
|
||||
) # Add each thread in a new line
|
||||
|
||||
# Source files on which the comments were made
|
||||
file = review_thread.get('path')
|
||||
if file and file not in files:
|
||||
files.append(file)
|
||||
|
||||
# If the comment ID is not provided or the thread contains the comment ID, add the thread to the list
|
||||
if comment_id is None or thread_contains_comment_id:
|
||||
unresolved_thread = ReviewThread(comment=message, files=files)
|
||||
review_threads.append(unresolved_thread)
|
||||
|
||||
@@ -344,6 +344,14 @@ async def resolve_issue(
|
||||
issue_numbers=[issue_number], comment_id=comment_id
|
||||
)
|
||||
|
||||
if not issues:
|
||||
raise ValueError(
|
||||
f'No issues found for issue number {issue_number}. Please verify that:\n'
|
||||
f'1. The issue/PR #{issue_number} exists in the repository {owner}/{repo}\n'
|
||||
f'2. You have the correct permissions to access it\n'
|
||||
f'3. The repository name is spelled correctly'
|
||||
)
|
||||
|
||||
issue = issues[0]
|
||||
|
||||
if comment_id is not None:
|
||||
|
||||
@@ -5,11 +5,11 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
import jinja2
|
||||
import litellm
|
||||
import requests
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.io_utils import (
|
||||
load_all_resolver_outputs,
|
||||
@@ -20,6 +20,12 @@ from openhands.resolver.resolver_output import ResolverOutput
|
||||
|
||||
|
||||
def apply_patch(repo_dir: str, patch: str) -> None:
|
||||
"""Apply a patch to a repository.
|
||||
|
||||
Args:
|
||||
repo_dir: The directory containing the repository
|
||||
patch: The patch to apply
|
||||
"""
|
||||
diffs = parse_patch(patch)
|
||||
for diff in diffs:
|
||||
if not diff.header.new_path:
|
||||
@@ -112,6 +118,14 @@ def apply_patch(repo_dir: str, patch: str) -> None:
|
||||
def initialize_repo(
|
||||
output_dir: str, issue_number: int, issue_type: str, base_commit: str | None = None
|
||||
) -> str:
|
||||
"""Initialize the repository.
|
||||
|
||||
Args:
|
||||
output_dir: The output directory to write the repository to
|
||||
issue_number: The issue number to fix
|
||||
issue_type: The type of the issue
|
||||
base_commit: The base commit to checkout (if issue_type is pr)
|
||||
"""
|
||||
src_dir = os.path.join(output_dir, 'repo')
|
||||
dest_dir = os.path.join(output_dir, 'patches', f'{issue_type}_{issue_number}')
|
||||
|
||||
@@ -124,6 +138,7 @@ def initialize_repo(
|
||||
shutil.copytree(src_dir, dest_dir)
|
||||
print(f'Copied repository to {dest_dir}')
|
||||
|
||||
# Checkout the base commit if provided
|
||||
if base_commit:
|
||||
result = subprocess.run(
|
||||
f'git -C {dest_dir} checkout {base_commit}',
|
||||
@@ -139,6 +154,13 @@ def initialize_repo(
|
||||
|
||||
|
||||
def make_commit(repo_dir: str, issue: GithubIssue, issue_type: str) -> None:
|
||||
"""Make a commit with the changes to the repository.
|
||||
|
||||
Args:
|
||||
repo_dir: The directory containing the repository
|
||||
issue: The issue to fix
|
||||
issue_type: The type of the issue
|
||||
"""
|
||||
# Check if git username is set
|
||||
result = subprocess.run(
|
||||
f'git -C {repo_dir} config user.name',
|
||||
@@ -158,6 +180,7 @@ def make_commit(repo_dir: str, issue: GithubIssue, issue_type: str) -> None:
|
||||
)
|
||||
print('Git user configured as openhands')
|
||||
|
||||
# Add all changes to the git index
|
||||
result = subprocess.run(
|
||||
f'git -C {repo_dir} add .', shell=True, capture_output=True, text=True
|
||||
)
|
||||
@@ -165,6 +188,7 @@ def make_commit(repo_dir: str, issue: GithubIssue, issue_type: str) -> None:
|
||||
print(f'Error adding files: {result.stderr}')
|
||||
raise RuntimeError('Failed to add files to git')
|
||||
|
||||
# Check the status of the git index
|
||||
status_result = subprocess.run(
|
||||
f'git -C {repo_dir} status --porcelain',
|
||||
shell=True,
|
||||
@@ -172,11 +196,15 @@ def make_commit(repo_dir: str, issue: GithubIssue, issue_type: str) -> None:
|
||||
text=True,
|
||||
)
|
||||
|
||||
# If there are no changes, raise an error
|
||||
if not status_result.stdout.strip():
|
||||
print(f'No changes to commit for issue #{issue.number}. Skipping commit.')
|
||||
raise RuntimeError('ERROR: Openhands failed to make code changes.')
|
||||
|
||||
# Prepare the commit message
|
||||
commit_message = f'Fix {issue_type} #{issue.number}: {issue.title}'
|
||||
|
||||
# Commit the changes
|
||||
result = subprocess.run(
|
||||
['git', '-C', repo_dir, 'commit', '-m', commit_message],
|
||||
capture_output=True,
|
||||
@@ -206,12 +234,24 @@ def send_pull_request(
|
||||
github_token: str,
|
||||
github_username: str | None,
|
||||
patch_dir: str,
|
||||
llm_config: LLMConfig,
|
||||
pr_type: str,
|
||||
fork_owner: str | None = None,
|
||||
additional_message: str | None = None,
|
||||
target_branch: str | None = None,
|
||||
reviewer: str | None = None,
|
||||
) -> str:
|
||||
"""Send a pull request to a GitHub repository.
|
||||
|
||||
Args:
|
||||
github_issue: The issue to send the pull request for
|
||||
github_token: The GitHub token to use for authentication
|
||||
github_username: The GitHub username, if provided
|
||||
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)
|
||||
"""
|
||||
if pr_type not in ['branch', 'draft', 'ready']:
|
||||
raise ValueError(f'Invalid pr_type: {pr_type}')
|
||||
|
||||
@@ -227,6 +267,7 @@ def send_pull_request(
|
||||
branch_name = base_branch_name
|
||||
attempt = 1
|
||||
|
||||
# Find a unique branch name
|
||||
print('Checking if branch exists...')
|
||||
while branch_exists(base_url, branch_name, headers):
|
||||
attempt += 1
|
||||
@@ -279,6 +320,7 @@ def send_pull_request(
|
||||
print(f'Error pushing changes: {result.stderr}')
|
||||
raise RuntimeError('Failed to push changes to the remote repository')
|
||||
|
||||
# Prepare the PR data: title and body
|
||||
pr_title = f'Fix issue #{github_issue.number}: {github_issue.title}'
|
||||
pr_body = f'This pull request fixes #{github_issue.number}.'
|
||||
if additional_message:
|
||||
@@ -290,6 +332,7 @@ def send_pull_request(
|
||||
if pr_type == 'branch':
|
||||
url = f'https://github.com/{push_owner}/{github_issue.repo}/compare/{branch_name}?expand=1'
|
||||
else:
|
||||
# Prepare the PR for the GitHub API
|
||||
data = {
|
||||
'title': pr_title, # No need to escape title for GitHub API
|
||||
'body': pr_body,
|
||||
@@ -297,6 +340,8 @@ def send_pull_request(
|
||||
'base': base_branch,
|
||||
'draft': pr_type == 'draft',
|
||||
}
|
||||
|
||||
# Send the PR and get its URL to tell the user
|
||||
response = requests.post(f'{base_url}/pulls', headers=headers, json=data)
|
||||
if response.status_code == 403:
|
||||
raise RuntimeError(
|
||||
@@ -306,6 +351,19 @@ def send_pull_request(
|
||||
response.raise_for_status()
|
||||
pr_data = response.json()
|
||||
|
||||
# Request review if a reviewer was specified
|
||||
if reviewer and pr_type != 'branch':
|
||||
review_data = {'reviewers': [reviewer]}
|
||||
review_response = requests.post(
|
||||
f'{base_url}/pulls/{pr_data["number"]}/requested_reviewers',
|
||||
headers=headers,
|
||||
json=review_data,
|
||||
)
|
||||
if review_response.status_code != 201:
|
||||
print(
|
||||
f'Warning: Failed to request review from {reviewer}: {review_response.text}'
|
||||
)
|
||||
|
||||
url = pr_data['html_url']
|
||||
|
||||
print(f'{pr_type} created: {url}\n\n--- Title: {pr_title}\n\n--- Body:\n{pr_body}')
|
||||
@@ -314,6 +372,13 @@ def send_pull_request(
|
||||
|
||||
|
||||
def reply_to_comment(github_token: str, comment_id: str, reply: str):
|
||||
"""Reply to a comment on a GitHub issue or pull request.
|
||||
|
||||
Args:
|
||||
github_token: The GitHub token to use for authentication
|
||||
comment_id: The ID of the comment to reply to
|
||||
reply: The reply message to post
|
||||
"""
|
||||
# Opting for graphql as REST API doesn't allow reply to replies in comment threads
|
||||
query = """
|
||||
mutation($body: String!, $pullRequestReviewThreadId: ID!) {
|
||||
@@ -327,6 +392,7 @@ def reply_to_comment(github_token: str, comment_id: str, reply: str):
|
||||
}
|
||||
"""
|
||||
|
||||
# Prepare the reply to the comment
|
||||
comment_reply = f'Openhands fix success summary\n\n\n{reply}'
|
||||
variables = {'body': comment_reply, 'pullRequestReviewThreadId': comment_id}
|
||||
url = 'https://api.github.com/graphql'
|
||||
@@ -335,6 +401,7 @@ def reply_to_comment(github_token: str, comment_id: str, reply: str):
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
# Send the reply to the comment
|
||||
response = requests.post(
|
||||
url, json={'query': query, 'variables': variables}, headers=headers
|
||||
)
|
||||
@@ -392,13 +459,14 @@ def update_existing_pull_request(
|
||||
base_url = f'https://api.github.com/repos/{github_issue.owner}/{github_issue.repo}'
|
||||
branch_name = github_issue.head_branch
|
||||
|
||||
# Push the changes to the existing branch
|
||||
# Prepare the push command
|
||||
push_command = (
|
||||
f'git -C {patch_dir} push '
|
||||
f'https://{github_username}:{github_token}@github.com/'
|
||||
f'{github_issue.owner}/{github_issue.repo}.git {branch_name}'
|
||||
)
|
||||
|
||||
# Push the changes to the existing branch
|
||||
result = subprocess.run(push_command, shell=True, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f'Error pushing changes: {result.stderr}')
|
||||
@@ -420,6 +488,7 @@ def update_existing_pull_request(
|
||||
|
||||
# Summarize with LLM if provided
|
||||
if llm_config is not None:
|
||||
llm = LLM(llm_config)
|
||||
with open(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
@@ -429,16 +498,13 @@ def update_existing_pull_request(
|
||||
) as f:
|
||||
template = jinja2.Template(f.read())
|
||||
prompt = template.render(comment_message=comment_message)
|
||||
response = litellm.completion(
|
||||
model=llm_config.model,
|
||||
response = llm.completion(
|
||||
messages=[{'role': 'user', 'content': prompt}],
|
||||
api_key=llm_config.api_key,
|
||||
base_url=llm_config.base_url,
|
||||
)
|
||||
comment_message = response.choices[0].message.content.strip()
|
||||
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
comment_message = 'New OpenHands update'
|
||||
comment_message = f'A new OpenHands update is available, but failed to parse or summarize the changes:\n{additional_message}'
|
||||
|
||||
# Post a comment on the PR
|
||||
if comment_message:
|
||||
@@ -468,6 +534,7 @@ def process_single_issue(
|
||||
fork_owner: str | None,
|
||||
send_on_failure: bool,
|
||||
target_branch: str | None = None,
|
||||
reviewer: str | None = None,
|
||||
) -> None:
|
||||
if not resolver_output.success and not send_on_failure:
|
||||
print(
|
||||
@@ -514,10 +581,10 @@ def process_single_issue(
|
||||
github_username=github_username,
|
||||
patch_dir=patched_repo_dir,
|
||||
pr_type=pr_type,
|
||||
llm_config=llm_config,
|
||||
fork_owner=fork_owner,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
target_branch=target_branch,
|
||||
reviewer=reviewer,
|
||||
)
|
||||
|
||||
|
||||
@@ -614,6 +681,12 @@ def main():
|
||||
default=None,
|
||||
help='Target branch to create the pull request against (defaults to repository default branch)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--reviewer',
|
||||
type=str,
|
||||
help='GitHub username of the person to request review from',
|
||||
default=None,
|
||||
)
|
||||
my_args = parser.parse_args()
|
||||
|
||||
github_token = (
|
||||
@@ -667,6 +740,7 @@ def main():
|
||||
my_args.fork_owner,
|
||||
my_args.send_on_failure,
|
||||
my_args.target_branch,
|
||||
my_args.reviewer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ class RuntimeBuilder(abc.ABC):
|
||||
path: str,
|
||||
tags: list[str],
|
||||
platform: str | None = None,
|
||||
extra_build_args: list[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Build the runtime image.
|
||||
"""Build the runtime image.
|
||||
|
||||
Args:
|
||||
path (str): The path to the runtime image's build directory.
|
||||
tags (list[str]): The tags to apply to the runtime image (e.g., ["repo:my-repo", "sha:my-sha"]).
|
||||
platform (str, optional): The target platform for the build. Defaults to None.
|
||||
extra_build_args (list[str], optional): Additional build arguments to pass to the builder. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The name:tag of the runtime image after build (e.g., "repo:sha").
|
||||
This can be different from the tags input if the builder chooses to mutate the tags (e.g., adding a
|
||||
@@ -28,8 +30,7 @@ class RuntimeBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def image_exists(self, image_name: str, pull_from_repo: bool = True) -> bool:
|
||||
"""
|
||||
Check if the runtime image exists.
|
||||
"""Check if the runtime image exists.
|
||||
|
||||
Args:
|
||||
image_name (str): The name of the runtime image (e.g., "repo:sha").
|
||||
|
||||
@@ -9,6 +9,7 @@ from openhands import __version__ as oh_version
|
||||
from openhands.core.logger import RollingLogger
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.runtime.builder.base import RuntimeBuilder
|
||||
from openhands.utils.term_color import TermColor, colorize
|
||||
|
||||
|
||||
class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
@@ -27,8 +28,8 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
path: str,
|
||||
tags: list[str],
|
||||
platform: str | None = None,
|
||||
use_local_cache: bool = False,
|
||||
extra_build_args: list[str] | None = None,
|
||||
use_local_cache: bool = False,
|
||||
) -> str:
|
||||
"""Builds a Docker image using BuildKit and handles the build logs appropriately.
|
||||
|
||||
@@ -187,7 +188,9 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
return True
|
||||
except docker.errors.ImageNotFound:
|
||||
if not pull_from_repo:
|
||||
logger.debug(f'Image {image_name} not found locally')
|
||||
logger.debug(
|
||||
f'Image {image_name} {colorize("not found", TermColor.WARNING)} locally'
|
||||
)
|
||||
return False
|
||||
try:
|
||||
logger.debug(
|
||||
@@ -214,7 +217,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
logger.debug('Could not find image locally or in registry.')
|
||||
return False
|
||||
except Exception as e:
|
||||
msg = 'Image could not be pulled: '
|
||||
msg = f'Image {colorize("could not be pulled", TermColor.ERROR)}: '
|
||||
ex_msg = str(e)
|
||||
if 'Not Found' in ex_msg:
|
||||
msg += 'image not found in registry.'
|
||||
@@ -286,8 +289,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
logger.debug(current_line['status'])
|
||||
|
||||
def _prune_old_cache_files(self, cache_dir: str, max_age_days: int = 7) -> None:
|
||||
"""
|
||||
Prune cache files older than the specified number of days.
|
||||
"""Prune cache files older than the specified number of days.
|
||||
|
||||
Args:
|
||||
cache_dir (str): The path to the cache directory.
|
||||
@@ -311,8 +313,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
logger.warning(f'Error during build cache pruning: {e}')
|
||||
|
||||
def _is_cache_usable(self, cache_dir: str) -> bool:
|
||||
"""
|
||||
Check if the cache directory is usable (exists and is writable).
|
||||
"""Check if the cache directory is usable (exists and is writable).
|
||||
|
||||
Args:
|
||||
cache_dir (str): The path to the cache directory.
|
||||
|
||||
@@ -23,7 +23,13 @@ class RemoteRuntimeBuilder(RuntimeBuilder):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({'X-API-Key': self.api_key})
|
||||
|
||||
def build(self, path: str, tags: list[str], platform: str | None = None) -> str:
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
tags: list[str],
|
||||
platform: str | None = None,
|
||||
extra_build_args: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Builds a Docker image using the Runtime API's /build endpoint."""
|
||||
# Create a tar archive of the build context
|
||||
tar_buffer = io.BytesIO()
|
||||
|
||||
@@ -229,6 +229,7 @@ class EventStreamRuntime(Runtime):
|
||||
platform=self.config.sandbox.platform,
|
||||
extra_deps=self.config.sandbox.runtime_extra_deps,
|
||||
force_rebuild=self.config.sandbox.force_rebuild_runtime,
|
||||
extra_build_args=self.config.sandbox.runtime_extra_build_args,
|
||||
)
|
||||
|
||||
self.log(
|
||||
|
||||
@@ -24,7 +24,7 @@ def send_request(
|
||||
timeout: int = 10,
|
||||
**kwargs: Any,
|
||||
) -> requests.Response:
|
||||
response = session.request(method, url, **kwargs)
|
||||
response = session.request(method, url, timeout=timeout, **kwargs)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
|
||||
@@ -111,6 +111,7 @@ def build_runtime_image(
|
||||
build_folder: str | None = None,
|
||||
dry_run: bool = False,
|
||||
force_rebuild: bool = False,
|
||||
extra_build_args: List[str] | None = None,
|
||||
) -> str:
|
||||
"""Prepares the final docker build folder.
|
||||
If dry_run is False, it will also build the OpenHands runtime Docker image using the docker build folder.
|
||||
@@ -123,6 +124,7 @@ def build_runtime_image(
|
||||
- build_folder (str): The directory to use for the build. If not provided a temporary directory will be used
|
||||
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
|
||||
- force_rebuild (bool): if True, it will create the Dockerfile which uses the base_image
|
||||
- extra_build_args (List[str]): Additional build arguments to pass to the builder
|
||||
|
||||
Returns:
|
||||
- str: <image_repo>:<MD5 hash>. Where MD5 hash is the hash of the docker build folder
|
||||
@@ -139,6 +141,7 @@ def build_runtime_image(
|
||||
dry_run=dry_run,
|
||||
force_rebuild=force_rebuild,
|
||||
platform=platform,
|
||||
extra_build_args=extra_build_args,
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -150,6 +153,7 @@ def build_runtime_image(
|
||||
dry_run=dry_run,
|
||||
force_rebuild=force_rebuild,
|
||||
platform=platform,
|
||||
extra_build_args=extra_build_args,
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -162,6 +166,7 @@ def build_runtime_image_in_folder(
|
||||
dry_run: bool,
|
||||
force_rebuild: bool,
|
||||
platform: str | None = None,
|
||||
extra_build_args: List[str] | None = None,
|
||||
) -> str:
|
||||
runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image)
|
||||
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image)}'
|
||||
@@ -193,6 +198,7 @@ def build_runtime_image_in_folder(
|
||||
lock_tag,
|
||||
versioned_tag,
|
||||
platform,
|
||||
extra_build_args=extra_build_args,
|
||||
)
|
||||
return hash_image_name
|
||||
|
||||
@@ -234,6 +240,7 @@ def build_runtime_image_in_folder(
|
||||
if build_from == BuildFromImageType.SCRATCH
|
||||
else None,
|
||||
platform=platform,
|
||||
extra_build_args=extra_build_args,
|
||||
)
|
||||
|
||||
return hash_image_name
|
||||
@@ -339,6 +346,7 @@ def _build_sandbox_image(
|
||||
lock_tag: str,
|
||||
versioned_tag: str | None,
|
||||
platform: str | None = None,
|
||||
extra_build_args: List[str] | None = None,
|
||||
):
|
||||
"""Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist"""
|
||||
names = [
|
||||
@@ -350,7 +358,10 @@ def _build_sandbox_image(
|
||||
names = [name for name in names if not runtime_builder.image_exists(name, False)]
|
||||
|
||||
image_name = runtime_builder.build(
|
||||
path=str(build_folder), tags=names, platform=platform
|
||||
path=str(build_folder),
|
||||
tags=names,
|
||||
platform=platform,
|
||||
extra_build_args=extra_build_args,
|
||||
)
|
||||
if not image_name:
|
||||
raise RuntimeError(f'Build failed for image {names}')
|
||||
|
||||
+10
-2
@@ -1,4 +1,5 @@
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
@@ -21,10 +22,17 @@ from openhands.server.routes.feedback import app as feedback_api_router
|
||||
from openhands.server.routes.files import app as files_api_router
|
||||
from openhands.server.routes.public import app as public_api_router
|
||||
from openhands.server.routes.security import app as security_api_router
|
||||
from openhands.server.shared import config
|
||||
from openhands.server.shared import config, session_manager
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan(app: FastAPI):
|
||||
async with session_manager:
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=_lifespan)
|
||||
app.add_middleware(
|
||||
LocalhostCORSMiddleware,
|
||||
allow_credentials=True,
|
||||
|
||||
@@ -52,6 +52,7 @@ class SessionManager:
|
||||
"""
|
||||
We use a redis backchannel to send actions between server nodes
|
||||
"""
|
||||
logger.debug('_redis_subscribe')
|
||||
redis_client = self._get_redis_client()
|
||||
pubsub = redis_client.pubsub()
|
||||
await pubsub.subscribe('oh_event')
|
||||
@@ -75,7 +76,7 @@ class SessionManager:
|
||||
|
||||
async def _process_message(self, message: dict):
|
||||
data = json.loads(message['data'])
|
||||
logger.info(f'got_published_message:{message}')
|
||||
logger.debug(f'got_published_message:{message}')
|
||||
sid = data['sid']
|
||||
message_type = data['message_type']
|
||||
if message_type == 'event':
|
||||
@@ -112,7 +113,7 @@ class SessionManager:
|
||||
elif message_type == 'session_closing':
|
||||
# Session closing event - We only get this in the event of graceful shutdown,
|
||||
# which can't be guaranteed - nodes can simply vanish unexpectedly!
|
||||
logger.info(f'session_closing:{sid}')
|
||||
logger.debug(f'session_closing:{sid}')
|
||||
for (
|
||||
connection_id,
|
||||
local_sid,
|
||||
@@ -142,7 +143,9 @@ class SessionManager:
|
||||
async def detach_from_conversation(self, conversation: Conversation):
|
||||
await conversation.disconnect()
|
||||
|
||||
async def init_or_join_session(self, sid: str, connection_id: str, session_init_data: SessionInitData):
|
||||
async def init_or_join_session(
|
||||
self, sid: str, connection_id: str, session_init_data: SessionInitData
|
||||
):
|
||||
await self.sio.enter_room(connection_id, ROOM_KEY.format(sid=sid))
|
||||
self.local_connection_id_to_session_id[connection_id] = sid
|
||||
|
||||
@@ -165,6 +168,7 @@ class SessionManager:
|
||||
flag = asyncio.Event()
|
||||
self._session_is_running_flags[sid] = flag
|
||||
try:
|
||||
logger.debug(f'publish:is_session_running:{sid}')
|
||||
await self._get_redis_client().publish(
|
||||
'oh_event',
|
||||
json.dumps(
|
||||
|
||||
@@ -102,6 +102,12 @@ class EmbeddingsLoader:
|
||||
azure_endpoint=llm_config.base_url,
|
||||
api_version=llm_config.api_version,
|
||||
)
|
||||
elif strategy == 'voyage':
|
||||
from llama_index.embeddings.voyageai import VoyageEmbedding
|
||||
|
||||
return VoyageEmbedding(
|
||||
model_name='voyage-code-3',
|
||||
)
|
||||
elif (strategy is not None) and (strategy.lower() == 'none'):
|
||||
# TODO: this works but is not elegant enough. The incentive is when
|
||||
# an agent using embeddings is not used, there is no reason we need to
|
||||
|
||||
@@ -19,7 +19,7 @@ def _register_signal_handler(sig: signal.Signals):
|
||||
original_handler = None
|
||||
|
||||
def handler(sig_: int, frame: FrameType | None):
|
||||
logger.info(f'shutdown_signal:{sig_}')
|
||||
logger.debug(f'shutdown_signal:{sig_}')
|
||||
global _should_exit
|
||||
_should_exit = True
|
||||
if original_handler:
|
||||
@@ -34,15 +34,15 @@ def _register_signal_handlers():
|
||||
return
|
||||
_should_exit = False
|
||||
|
||||
logger.info('_register_signal_handlers')
|
||||
logger.debug('_register_signal_handlers')
|
||||
|
||||
# Check if we're in the main thread of the main interpreter
|
||||
if threading.current_thread() is threading.main_thread():
|
||||
logger.info('_register_signal_handlers:main_thread')
|
||||
logger.debug('_register_signal_handlers:main_thread')
|
||||
for sig in HANDLED_SIGNALS:
|
||||
_register_signal_handler(sig)
|
||||
else:
|
||||
logger.info('_register_signal_handlers:not_main_thread')
|
||||
logger.debug('_register_signal_handlers:not_main_thread')
|
||||
|
||||
|
||||
def should_exit() -> bool:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from enum import Enum
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
class TermColor(Enum):
|
||||
"""Terminal color codes."""
|
||||
|
||||
WARNING = 'yellow'
|
||||
SUCCESS = 'green'
|
||||
ERROR = 'red'
|
||||
INFO = 'blue'
|
||||
|
||||
|
||||
def colorize(text: str, color: TermColor = TermColor.WARNING) -> str:
|
||||
"""Colorize text with specified color.
|
||||
|
||||
Args:
|
||||
text (str): Text to be colored
|
||||
color (TermColor, optional): Color to use. Defaults to TermColor.WARNING
|
||||
|
||||
Returns:
|
||||
str: Colored text
|
||||
"""
|
||||
return colored(text, color.value)
|
||||
Generated
+70
-36
@@ -553,17 +553,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.35.68"
|
||||
version = "1.35.78"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "boto3-1.35.68-py3-none-any.whl", hash = "sha256:9b26fa31901da7793c1dcd65eee9bab7e897d8aa1ffed0b5e1c3bce93d2aefe4"},
|
||||
{file = "boto3-1.35.68.tar.gz", hash = "sha256:091d6bed1422370987a839bff3f8755df7404fc15e9fac2a48e8505356f07433"},
|
||||
{file = "boto3-1.35.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"},
|
||||
{file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.35.68,<1.36.0"
|
||||
botocore = ">=1.35.78,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@@ -572,13 +572,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.68"
|
||||
version = "1.35.78"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.35.68-py3-none-any.whl", hash = "sha256:599139d5564291f5be873800711f9e4e14a823395ae9ce7b142be775e9849b94"},
|
||||
{file = "botocore-1.35.68.tar.gz", hash = "sha256:42c3700583a82f2b5316281a073d644a521d6358837e2b446dc458ba5d990fb4"},
|
||||
{file = "botocore-1.35.78-py3-none-any.whl", hash = "sha256:41c37bd7c0326f25122f33ec84fb80fc0a14d7fcc9961431b0e57568e88c9cb5"},
|
||||
{file = "botocore-1.35.78.tar.gz", hash = "sha256:6905036c25449ae8dba5e950e4b908e4b8a6fe6b516bf61e007ecb62fa21f323"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3739,22 +3739,23 @@ types-tqdm = "*"
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.52.15"
|
||||
version = "1.54.1"
|
||||
description = "Library to easily interface with LLM API providers"
|
||||
optional = false
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
files = [
|
||||
{file = "litellm-1.52.15-py3-none-any.whl", hash = "sha256:8a2d8e2526c5e7afb3006b0214d3c348778462fefafd582fd76bb7f5c35d28d0"},
|
||||
{file = "litellm-1.52.15.tar.gz", hash = "sha256:11a61b1b033ddff9d480da66c00acc9d3e4fbfeed166d1b0de8eda16c684116e"},
|
||||
{file = "litellm-1.54.1-py3-none-any.whl", hash = "sha256:d8e60d4a5e8decb0234a1e8c20351c904aec561fb4025df7df3d0d7ea81ca442"},
|
||||
{file = "litellm-1.54.1.tar.gz", hash = "sha256:b5a8fc99160fab0699b9258457432b3975499218ffcf1b515709808b2ce5a2d7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = "*"
|
||||
click = "*"
|
||||
httpx = ">=0.23.0,<0.28.0"
|
||||
importlib-metadata = ">=6.8.0"
|
||||
jinja2 = ">=3.1.2,<4.0.0"
|
||||
jsonschema = ">=4.22.0,<5.0.0"
|
||||
openai = ">=1.54.0"
|
||||
openai = ">=1.55.3"
|
||||
pydantic = ">=2.0.0,<3.0.0"
|
||||
python-dotenv = ">=0.2.0"
|
||||
requests = ">=2.31.0,<3.0.0"
|
||||
@@ -3782,24 +3783,24 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index"
|
||||
version = "0.12.3"
|
||||
version = "0.12.5"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index-0.12.3-py3-none-any.whl", hash = "sha256:0fe8836c84becf05bb95c19aaf15c643bd57a5cb324088a84f2f299a779e97bb"},
|
||||
{file = "llama_index-0.12.3.tar.gz", hash = "sha256:d3ea4d599225c934ff9e56712203f5236e7e143eaf2144d92238f37f1de24ccd"},
|
||||
{file = "llama_index-0.12.5-py3-none-any.whl", hash = "sha256:2bb6d234cf6d7fdb6a308e9aff1a607e83a24210cc7325be62c65bc43493680f"},
|
||||
{file = "llama_index-0.12.5.tar.gz", hash = "sha256:a816f18079c88e17b53fab6efc27f7c3dfb0a7af559afaaeaeef0577654235a4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-agent-openai = ">=0.4.0,<0.5.0"
|
||||
llama-index-cli = ">=0.4.0,<0.5.0"
|
||||
llama-index-core = ">=0.12.3,<0.13.0"
|
||||
llama-index-core = ">=0.12.5,<0.13.0"
|
||||
llama-index-embeddings-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-indices-managed-llama-cloud = ">=0.4.0"
|
||||
llama-index-legacy = ">=0.9.48,<0.10.0"
|
||||
llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-multi-modal-llms-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-multi-modal-llms-openai = ">=0.4.0,<0.5.0"
|
||||
llama-index-program-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-question-gen-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-readers-file = ">=0.4.0,<0.5.0"
|
||||
@@ -3840,13 +3841,13 @@ llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-core"
|
||||
version = "0.12.3"
|
||||
version = "0.12.5"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_core-0.12.3-py3-none-any.whl", hash = "sha256:f0034965e74f508594cb96b8ebb17ce123d73d5f8858adcf38f5cdd936b94262"},
|
||||
{file = "llama_index_core-0.12.3.tar.gz", hash = "sha256:61fa0a1155a022b5b63c081d6709f5b57bae231b1c847c78e2052c93a231b90a"},
|
||||
{file = "llama_index_core-0.12.5-py3-none-any.whl", hash = "sha256:1fe6dd39b2dc5a945b4702d780a2f5962a553e187524a255429461dc92a664db"},
|
||||
{file = "llama_index_core-0.12.5.tar.gz", hash = "sha256:1d967323891920579fad3d6497587c137894df3f76718a3ec134f9201f2f4fc0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3862,11 +3863,11 @@ networkx = ">=3.0"
|
||||
nltk = ">3.8.1"
|
||||
numpy = "*"
|
||||
pillow = ">=9.0.0"
|
||||
pydantic = ">=2.7.0,<2.10.0"
|
||||
pydantic = ">=2.8.0"
|
||||
PyYAML = ">=6.0.1"
|
||||
requests = ">=2.31.0"
|
||||
SQLAlchemy = {version = ">=1.4.49", extras = ["asyncio"]}
|
||||
tenacity = ">=8.2.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
tenacity = ">=8.2.0,<8.4.0 || >8.4.0,<10.0.0"
|
||||
tiktoken = ">=0.3.3"
|
||||
tqdm = ">=4.66.1,<5.0.0"
|
||||
typing-extensions = ">=4.5.0"
|
||||
@@ -3907,18 +3908,18 @@ sentence-transformers = ">=2.6.1"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-embeddings-ollama"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "llama-index embeddings ollama integration"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_embeddings_ollama-0.4.0-py3-none-any.whl", hash = "sha256:198706882bad4c91db8250b5bdb47fd4eb9f871e0825aff64361b09210a8087c"},
|
||||
{file = "llama_index_embeddings_ollama-0.4.0.tar.gz", hash = "sha256:ebe73354470f8b6061d5f961b1ee50716d54972ef295fb253e3ebc393d5b36cb"},
|
||||
{file = "llama_index_embeddings_ollama-0.5.0-py3-none-any.whl", hash = "sha256:843ecccfbe2db548a39e71a85e8ebbfe3cf2659db9533c080dcb291e4975af3b"},
|
||||
{file = "llama_index_embeddings_ollama-0.5.0.tar.gz", hash = "sha256:fec8fa249ed2fb13912e1511decb21c025a53294728a21f25bd2d5f30f435a94"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-core = ">=0.12.0,<0.13.0"
|
||||
ollama = ">=0.3.1,<0.4.0"
|
||||
ollama = ">=0.3.1"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-embeddings-openai"
|
||||
@@ -3935,6 +3936,21 @@ files = [
|
||||
llama-index-core = ">=0.12.0,<0.13.0"
|
||||
openai = ">=1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-embeddings-voyageai"
|
||||
version = "0.3.1"
|
||||
description = "llama-index embeddings voyageai integration"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_embeddings_voyageai-0.3.1-py3-none-any.whl", hash = "sha256:f0e0b327ab21669a2b0501f207a6862f7a0b0a115bff15b6ceac712273a6fa03"},
|
||||
{file = "llama_index_embeddings_voyageai-0.3.1.tar.gz", hash = "sha256:cfbc0a0697bda39c18398418628596c6ae8c668a0306d504a4fc16100fcd7d57"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-core = ">=0.12.0,<0.13.0"
|
||||
voyageai = {version = ">=0.2.1,<0.4.0", markers = "python_version >= \"3.9\" and python_version < \"3.13\""}
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-indices-managed-llama-cloud"
|
||||
version = "0.6.2"
|
||||
@@ -4023,17 +4039,17 @@ openai = ">=1.40.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-multi-modal-llms-openai"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "llama-index multi-modal-llms openai integration"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_multi_modal_llms_openai-0.3.0-py3-none-any.whl", hash = "sha256:9b7e3e39b19b2668b9c75014bcb90795bb546f0f9e1af8b7f1087f8687805763"},
|
||||
{file = "llama_index_multi_modal_llms_openai-0.3.0.tar.gz", hash = "sha256:71e983c7771c39088e4058cd78029219315a0fb631b9e12b903e53243b9a3fd6"},
|
||||
{file = "llama_index_multi_modal_llms_openai-0.4.0-py3-none-any.whl", hash = "sha256:c5bda1b3c6d14eee87a819ba72b122d82877829695dce8f90a8c600ac16ce243"},
|
||||
{file = "llama_index_multi_modal_llms_openai-0.4.0.tar.gz", hash = "sha256:11c3ac7e2d7ace9dbcdd9a662f27bca5fefce98c5682abaffb7dd01d59776658"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-core = ">=0.12.0,<0.13.0"
|
||||
llama-index-core = ">=0.12.3,<0.13.0"
|
||||
llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
@@ -4106,13 +4122,13 @@ llama-parse = ">=0.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-vector-stores-chroma"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
description = "llama-index vector_stores chroma integration"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_vector_stores_chroma-0.4.0-py3-none-any.whl", hash = "sha256:058986b80df0d5f71f435df2a58f5b8e21feebdb34a56e4dbced8c2c225d10b6"},
|
||||
{file = "llama_index_vector_stores_chroma-0.4.0.tar.gz", hash = "sha256:c5e979591d09adc91f09b08fb1f523d3cfe30eee3cf13e53da28540caea7366a"},
|
||||
{file = "llama_index_vector_stores_chroma-0.4.1-py3-none-any.whl", hash = "sha256:42200af4fa5c8df820b865825d1b506cecb922c8fbd7421eda8a5609b390c1d5"},
|
||||
{file = "llama_index_vector_stores_chroma-0.4.1.tar.gz", hash = "sha256:70ee74ccf304adda04171d014e483759c68a1c92f679ea2ca2e6b6f45b6fef08"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5413,13 +5429,13 @@ sympy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.55.0"
|
||||
version = "1.57.2"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "openai-1.55.0-py3-none-any.whl", hash = "sha256:446e08918f8dd70d8723274be860404c8c7cc46b91b93bbc0ef051f57eb503c1"},
|
||||
{file = "openai-1.55.0.tar.gz", hash = "sha256:6c0975ac8540fe639d12b4ff5a8e0bf1424c844c4a4251148f59f06c4b2bd5db"},
|
||||
{file = "openai-1.57.2-py3-none-any.whl", hash = "sha256:f7326283c156fdee875746e7e54d36959fb198eadc683952ee05e3302fbd638d"},
|
||||
{file = "openai-1.57.2.tar.gz", hash = "sha256:5f49fd0f38e9f2131cda7deb45dafdd1aee4f52a637e190ce0ecf40147ce8cee"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -9359,6 +9375,24 @@ platformdirs = ">=3.9.1,<5"
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "voyageai"
|
||||
version = "0.2.4"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.1"
|
||||
files = [
|
||||
{file = "voyageai-0.2.4-py3-none-any.whl", hash = "sha256:e3070e5c78dec89adae43231334b4637aa88933dad99b1c33d3219fdfc94dfa4"},
|
||||
{file = "voyageai-0.2.4.tar.gz", hash = "sha256:b9911d8629e8a4e363291c133482fead49a3536afdf1e735f3ab3aaccd8d250d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.5,<4.0"
|
||||
aiolimiter = ">=1.1.0,<2.0.0"
|
||||
numpy = ">=1.11"
|
||||
requests = ">=2.20,<3.0"
|
||||
tenacity = ">=8.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
@@ -10060,4 +10094,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "ff3daee70a197e3f6ff460bd1e14be7ed443a100805947ee18df7afb7d898584"
|
||||
content-hash = "7334dd947fe93756227b5fc8f86303852c5e9aaf8787cc35b0ce23fc1540df67"
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,7 @@ packages = [
|
||||
python = "^3.12"
|
||||
datasets = "*"
|
||||
pandas = "*"
|
||||
litellm = "^1.52.3"
|
||||
litellm = "^1.54.1"
|
||||
google-generativeai = "*" # To use litellm with Gemini Pro API
|
||||
google-api-python-client = "*" # For Google Sheets API
|
||||
google-auth-httplib2 = "*" # For Google Sheets authentication
|
||||
@@ -76,6 +76,8 @@ llama-index-embeddings-huggingface = "*"
|
||||
torch = "2.5.1"
|
||||
llama-index-embeddings-azure-openai = "*"
|
||||
llama-index-embeddings-ollama = "*"
|
||||
voyageai = "*"
|
||||
llama-index-embeddings-voyageai = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "0.8.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ def mock_llm_response(content):
|
||||
|
||||
|
||||
def test_guess_success_review_threads_litellm_call():
|
||||
"""Test that the litellm.completion() call for review threads contains the expected content."""
|
||||
"""Test that the completion() call for review threads contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
|
||||
@@ -77,7 +77,7 @@ The changes successfully address the feedback."""
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(issue, history)
|
||||
|
||||
# Verify the litellm.completion() calls
|
||||
# Verify the completion() calls
|
||||
assert mock_completion.call_count == 2 # One call per review thread
|
||||
|
||||
# Check first call
|
||||
@@ -121,7 +121,7 @@ The changes successfully address the feedback."""
|
||||
|
||||
|
||||
def test_guess_success_thread_comments_litellm_call():
|
||||
"""Test that the litellm.completion() call for thread comments contains the expected content."""
|
||||
"""Test that the completion() call for thread comments contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
|
||||
@@ -176,7 +176,7 @@ The changes successfully address the feedback."""
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(issue, history)
|
||||
|
||||
# Verify the litellm.completion() call
|
||||
# Verify the completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
@@ -270,7 +270,7 @@ Changes look good"""
|
||||
review_thread, issues_context, last_message
|
||||
)
|
||||
|
||||
# Verify the litellm.completion() call
|
||||
# Verify the completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
@@ -326,7 +326,7 @@ Changes look good"""
|
||||
thread_comments, issues_context, last_message
|
||||
)
|
||||
|
||||
# Verify the litellm.completion() call
|
||||
# Verify the completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
@@ -379,7 +379,7 @@ Changes look good"""
|
||||
review_comments, issues_context, last_message
|
||||
)
|
||||
|
||||
# Verify the litellm.completion() call
|
||||
# Verify the completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
@@ -395,7 +395,7 @@ Changes look good"""
|
||||
|
||||
|
||||
def test_guess_success_review_comments_litellm_call():
|
||||
"""Test that the litellm.completion() call for review comments contains the expected content."""
|
||||
"""Test that the completion() call for review comments contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
|
||||
@@ -447,7 +447,7 @@ The changes successfully address the feedback."""
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(issue, history)
|
||||
|
||||
# Verify the litellm.completion() call
|
||||
# Verify the completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
@@ -153,7 +153,6 @@ def test_pr_title_with_quotes(monkeypatch):
|
||||
|
||||
# Try to send a PR - this will fail if the title is incorrectly escaped
|
||||
print('Sending PR...')
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.resolver.send_pull_request import send_pull_request
|
||||
|
||||
send_pull_request(
|
||||
@@ -161,6 +160,5 @@ def test_pr_title_with_quotes(monkeypatch):
|
||||
github_token='dummy-token',
|
||||
github_username='test-user',
|
||||
patch_dir=temp_dir,
|
||||
llm_config=LLMConfig(model='test-model', api_key='test-key'),
|
||||
pr_type='ready',
|
||||
)
|
||||
|
||||
@@ -84,6 +84,41 @@ def test_initialize_runtime():
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_issue_no_issues_found():
|
||||
from openhands.resolver.resolve_issue import resolve_issue
|
||||
|
||||
# Mock dependencies
|
||||
mock_handler = MagicMock()
|
||||
mock_handler.get_converted_issues.return_value = [] # Return empty list
|
||||
|
||||
with patch(
|
||||
'openhands.resolver.resolve_issue.issue_handler_factory',
|
||||
return_value=mock_handler,
|
||||
):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await resolve_issue(
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
token='test-token',
|
||||
username='test-user',
|
||||
max_iterations=5,
|
||||
output_dir='/tmp',
|
||||
llm_config=LLMConfig(model='test', api_key='test'),
|
||||
runtime_container_image='test-image',
|
||||
prompt_template='test-template',
|
||||
issue_type='pr',
|
||||
repo_instruction=None,
|
||||
issue_number=5432,
|
||||
comment_id=None,
|
||||
)
|
||||
|
||||
assert 'No issues found for issue number 5432' in str(exc_info.value)
|
||||
assert 'test-owner/test-repo' in str(exc_info.value)
|
||||
assert 'exists in the repository' in str(exc_info.value)
|
||||
assert 'correct permissions' in str(exc_info.value)
|
||||
|
||||
|
||||
def test_download_issues_from_github():
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
handler = IssueHandler('owner', 'repo', 'token', llm_config)
|
||||
|
||||
@@ -244,8 +244,12 @@ def test_initialize_repo(mock_output_dir):
|
||||
@patch('openhands.resolver.send_pull_request.reply_to_comment')
|
||||
@patch('requests.post')
|
||||
@patch('subprocess.run')
|
||||
@patch('openhands.resolver.send_pull_request.LLM')
|
||||
def test_update_existing_pull_request(
|
||||
mock_subprocess_run, mock_requests_post, mock_reply_to_comment
|
||||
mock_llm_class,
|
||||
mock_subprocess_run,
|
||||
mock_requests_post,
|
||||
mock_reply_to_comment,
|
||||
):
|
||||
# Arrange: Set up test data
|
||||
github_issue = GithubIssue(
|
||||
@@ -267,23 +271,28 @@ def test_update_existing_pull_request(
|
||||
|
||||
# Mock the requests.post call for adding a PR comment
|
||||
mock_requests_post.return_value.status_code = 201
|
||||
|
||||
# Mock LLM instance and completion call
|
||||
mock_llm_instance = MagicMock()
|
||||
mock_completion_response = MagicMock()
|
||||
mock_completion_response.choices = [
|
||||
MagicMock(message=MagicMock(content='This is an issue resolution.'))
|
||||
]
|
||||
mock_llm_instance.completion.return_value = mock_completion_response
|
||||
mock_llm_class.return_value = mock_llm_instance
|
||||
|
||||
llm_config = LLMConfig()
|
||||
|
||||
# Act: Call the function without comment_message to test auto-generation
|
||||
with patch('litellm.completion', MagicMock(return_value=mock_completion_response)):
|
||||
result = update_existing_pull_request(
|
||||
github_issue,
|
||||
github_token,
|
||||
github_username,
|
||||
patch_dir,
|
||||
llm_config,
|
||||
comment_message=None,
|
||||
additional_message=additional_message,
|
||||
)
|
||||
result = update_existing_pull_request(
|
||||
github_issue,
|
||||
github_token,
|
||||
github_username,
|
||||
patch_dir,
|
||||
llm_config,
|
||||
comment_message=None,
|
||||
additional_message=additional_message,
|
||||
)
|
||||
|
||||
# Assert: Check if the git push command was executed
|
||||
push_command = (
|
||||
@@ -342,7 +351,6 @@ def test_send_pull_request(
|
||||
mock_run,
|
||||
mock_github_issue,
|
||||
mock_output_dir,
|
||||
mock_llm_config,
|
||||
pr_type,
|
||||
target_branch,
|
||||
):
|
||||
@@ -377,7 +385,6 @@ def test_send_pull_request(
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type=pr_type,
|
||||
llm_config=mock_llm_config,
|
||||
target_branch=target_branch,
|
||||
)
|
||||
|
||||
@@ -425,9 +432,72 @@ def test_send_pull_request(
|
||||
assert post_data['draft'] == (pr_type == 'draft')
|
||||
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
def test_send_pull_request_with_reviewer(
|
||||
mock_get, mock_post, mock_run, mock_github_issue, mock_output_dir
|
||||
):
|
||||
repo_path = os.path.join(mock_output_dir, 'repo')
|
||||
reviewer = 'test-reviewer'
|
||||
|
||||
# Mock API responses
|
||||
mock_get.side_effect = [
|
||||
MagicMock(status_code=404), # Branch doesn't exist
|
||||
MagicMock(json=lambda: {'default_branch': 'main'}), # Get default branch
|
||||
]
|
||||
|
||||
# Mock PR creation response
|
||||
mock_post.side_effect = [
|
||||
MagicMock(
|
||||
status_code=201,
|
||||
json=lambda: {
|
||||
'html_url': 'https://github.com/test-owner/test-repo/pull/1',
|
||||
'number': 1,
|
||||
},
|
||||
), # PR creation
|
||||
MagicMock(status_code=201), # Reviewer request
|
||||
]
|
||||
|
||||
# Mock subprocess.run calls
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=0), # git checkout -b
|
||||
MagicMock(returncode=0), # git push
|
||||
]
|
||||
|
||||
# Call the function with reviewer
|
||||
result = send_pull_request(
|
||||
github_issue=mock_github_issue,
|
||||
github_token='test-token',
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type='ready',
|
||||
reviewer=reviewer,
|
||||
)
|
||||
|
||||
# Assert API calls
|
||||
assert mock_get.call_count == 2
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
# Check PR creation
|
||||
pr_create_call = mock_post.call_args_list[0]
|
||||
assert pr_create_call[1]['json']['title'] == 'Fix issue #42: Test Issue'
|
||||
|
||||
# Check reviewer request
|
||||
reviewer_request_call = mock_post.call_args_list[1]
|
||||
assert (
|
||||
reviewer_request_call[0][0]
|
||||
== 'https://api.github.com/repos/test-owner/test-repo/pulls/1/requested_reviewers'
|
||||
)
|
||||
assert reviewer_request_call[1]['json'] == {'reviewers': ['test-reviewer']}
|
||||
|
||||
# Check the result URL
|
||||
assert result == 'https://github.com/test-owner/test-repo/pull/1'
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
def test_send_pull_request_invalid_target_branch(
|
||||
mock_get, mock_github_issue, mock_output_dir, mock_llm_config
|
||||
mock_get, mock_github_issue, mock_output_dir
|
||||
):
|
||||
"""Test that an error is raised when specifying a non-existent target branch"""
|
||||
repo_path = os.path.join(mock_output_dir, 'repo')
|
||||
@@ -448,7 +518,6 @@ def test_send_pull_request_invalid_target_branch(
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type='ready',
|
||||
llm_config=mock_llm_config,
|
||||
target_branch='nonexistent-branch',
|
||||
)
|
||||
|
||||
@@ -460,7 +529,7 @@ def test_send_pull_request_invalid_target_branch(
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
def test_send_pull_request_git_push_failure(
|
||||
mock_get, mock_post, mock_run, mock_github_issue, mock_output_dir, mock_llm_config
|
||||
mock_get, mock_post, mock_run, mock_github_issue, mock_output_dir
|
||||
):
|
||||
repo_path = os.path.join(mock_output_dir, 'repo')
|
||||
|
||||
@@ -483,7 +552,6 @@ def test_send_pull_request_git_push_failure(
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type='ready',
|
||||
llm_config=mock_llm_config,
|
||||
)
|
||||
|
||||
# Assert that subprocess.run was called twice
|
||||
@@ -519,7 +587,7 @@ def test_send_pull_request_git_push_failure(
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
def test_send_pull_request_permission_error(
|
||||
mock_get, mock_post, mock_run, mock_github_issue, mock_output_dir, mock_llm_config
|
||||
mock_get, mock_post, mock_run, mock_github_issue, mock_output_dir
|
||||
):
|
||||
repo_path = os.path.join(mock_output_dir, 'repo')
|
||||
|
||||
@@ -543,7 +611,6 @@ def test_send_pull_request_permission_error(
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type='ready',
|
||||
llm_config=mock_llm_config,
|
||||
)
|
||||
|
||||
# Assert that the branch was created and pushed
|
||||
@@ -757,10 +824,10 @@ def test_process_single_issue(
|
||||
github_username=github_username,
|
||||
patch_dir=f'{mock_output_dir}/patches/issue_1',
|
||||
pr_type=pr_type,
|
||||
llm_config=mock_llm_config,
|
||||
fork_owner=None,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
target_branch=None,
|
||||
reviewer=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -940,7 +1007,7 @@ def test_process_all_successful_issues(
|
||||
@patch('requests.get')
|
||||
@patch('subprocess.run')
|
||||
def test_send_pull_request_branch_naming(
|
||||
mock_run, mock_get, mock_github_issue, mock_output_dir, mock_llm_config
|
||||
mock_run, mock_get, mock_github_issue, mock_output_dir
|
||||
):
|
||||
repo_path = os.path.join(mock_output_dir, 'repo')
|
||||
|
||||
@@ -965,7 +1032,6 @@ def test_send_pull_request_branch_naming(
|
||||
github_username='test-user',
|
||||
patch_dir=repo_path,
|
||||
pr_type='branch',
|
||||
llm_config=mock_llm_config,
|
||||
)
|
||||
|
||||
# Assert API calls
|
||||
@@ -1029,6 +1095,7 @@ def test_main(
|
||||
mock_args.llm_base_url = 'mock_url'
|
||||
mock_args.llm_api_key = 'mock_key'
|
||||
mock_args.target_branch = None
|
||||
mock_args.reviewer = None
|
||||
mock_parser.return_value.parse_args.return_value = mock_args
|
||||
|
||||
# Setup environment variables
|
||||
@@ -1063,6 +1130,7 @@ def test_main(
|
||||
None,
|
||||
False,
|
||||
mock_args.target_branch,
|
||||
mock_args.reviewer,
|
||||
)
|
||||
|
||||
# Other assertions
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from openhands.events.observation.commands import (
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
|
||||
|
||||
def test_cmd_output_success():
|
||||
# Test successful command
|
||||
obs = CmdOutputObservation(
|
||||
command_id=1, command='ls', content='file1.txt\nfile2.txt', exit_code=0
|
||||
)
|
||||
assert obs.success is True
|
||||
assert obs.error is False
|
||||
|
||||
# Test failed command
|
||||
obs = CmdOutputObservation(
|
||||
command_id=2, command='ls', content='No such file or directory', exit_code=1
|
||||
)
|
||||
assert obs.success is False
|
||||
assert obs.error is True
|
||||
|
||||
|
||||
def test_ipython_cell_success():
|
||||
# IPython cells are always successful
|
||||
obs = IPythonRunCellObservation(code='print("Hello")', content='Hello')
|
||||
assert obs.success is True
|
||||
assert obs.error is False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user