Compare commits

..

23 Commits

Author SHA1 Message Date
openhands 81c5c204db Fix markdown code block formatting in chat window 2024-12-13 14:55:27 +00:00
Ryan H. Tran 8ae2fb636e Remove symlink use for swebench setup (#5549) 2024-12-13 22:18:14 +08:00
sp.wack de75bd0690 fix(frontend): Prevent VSCode from opening when remounting (#5544) 2024-12-13 09:35:34 +04:00
tofarr 2fb45d410d Fix: Making the logs quieter (#5525) 2024-12-12 19:36:13 -07:00
mamoodi 8300cf0436 Specify unsupported paths for installing OpenHands (#5540) 2024-12-12 16:26:18 -05:00
mamoodi 7dd2bc569f Restart troubleshooting documentation. (#5317) 2024-12-12 15:49:18 -05:00
Robert Brennan 6e1fae29c9 Add note about design partner program to README (#5570) 2024-12-12 20:13:07 +00:00
sp.wack 19525a487c fix(frontend): Trim settings data when setting to storage (#5567) 2024-12-12 22:36:17 +04:00
Engel Nyst 7d0405282a Apply context window truncation for certain bad requests (#5566) 2024-12-12 18:11:59 +00:00
OpenHands 92c166551f Fix issue #5563: [Bug]: Prompt is not deleted when the user sends a message (#5564) 2024-12-12 10:06:40 -08:00
Xingyao Wang ebb68b33db Fix issue #5527: Document repository customization and micro-agents (#5528)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-12 09:47:28 -08:00
sp.wack 37c46f1ed8 fix(frontend): Prevent push message from being rendered twice (#5546) 2024-12-12 09:19:48 -08:00
Engel Nyst ac5190c283 Add voyage ai embeddings (#5547) 2024-12-12 09:19:05 -08:00
dependabot[bot] ed3916b79b chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.61.4 to 5.62.1 in /frontend in the eslint group (#5556)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 15:30:02 +00:00
mamoodi 27a647cd3e Release 0.15.2 (#5552) 2024-12-12 10:09:47 -05:00
sp.wack 42a536d450 Revert "chore(deps): bump the version-all group across 1 directory with 30 updates" (#5548) 2024-12-12 13:48:57 +04:00
dependabot[bot] 41e564dc41 chore(deps): bump the version-all group across 1 directory with 30 updates (#5522)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-12-12 12:18:26 +04:00
Graham Neubig e979f51ea5 Fix chat input not clearing after image paste/drop (#5342)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-11 22:18:38 -08:00
Engel Nyst 425ccb0fbb Clean up empty content fix (revert #4935) (#5539) 2024-12-12 02:48:06 +00:00
Cheng Yang 7e4c1c733b feat(sandbox): add support for extra Docker build arguments (#5447) 2024-12-12 10:21:46 +08:00
Engel Nyst ffd472d6b8 Update litellm (#5520) 2024-12-12 03:12:50 +01:00
mamoodi 2f2ea9ec91 Update the doc for headless to include no continue (#5537) 2024-12-12 02:03:06 +00:00
OpenHands 6a6ce5f3ee Fix issue #5478: Add color to the line next to "Ran a XXX Command" based on return value (#5483)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-12-11 23:20:29 +00:00
64 changed files with 1027 additions and 711 deletions
-1
View File
@@ -1 +0,0 @@
The files in this directory configure a development container for GitHub Codespaces.
-15
View File
@@ -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 &'"
}
-6
View File
@@ -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 -
+5
View File
@@ -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.
![App screenshot](./docs/static/img/screenshot.png)
## ⚡ Quick Start
+3
View File
@@ -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
+3
View File
@@ -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
@@ -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.
@@ -7,7 +7,6 @@
:::tip
OpenHands 仅通过 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) 支持 Windows。
请确保在您的 WSL 终端内运行所有命令。
查看 [Windows 用户的 WSL 注意事项](troubleshooting/windows) 以获取一些故障排除指南。
:::
## 常见问题
@@ -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`
网络问题应该得到解决。
+1 -1
View File
@@ -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"
```
+213
View File
@@ -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,192 +1,37 @@
# 🚧 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)
* [Error building runtime docker image](#error-building-runtime-docker-image)
**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**
```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'}}
```
**Details**
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.
---
# Development Workflow Specific
### Error building runtime docker image
**Symptoms**
Attempts to start a new session fail, and an errors with terms like the following appear in the logs:
* `debian-security bookworm-security`
* `InRelease At least one invalid signature was encountered.`
**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.
```
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:
@@ -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.
-71
View File
@@ -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`.
+19 -3
View File
@@ -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',
},
],
},
{
@@ -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
@@ -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();
});
});
+6 -6
View File
@@ -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.1",
"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"
+2 -2
View File
@@ -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",
@@ -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,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);
};
+3 -33
View File
@@ -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;
};
@@ -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

+4
View File
@@ -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

+18 -2
View File
@@ -27,7 +27,7 @@ code {
monospace;
}
.markdown-body code {
.markdown-body :not(pre) > code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
@@ -39,9 +39,25 @@ code {
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 {
+1
View File
@@ -4,6 +4,7 @@ type Message = {
timestamp: string;
imageUrls?: string[];
type?: "thought" | "error" | "action";
success?: boolean;
pending?: boolean;
translationID?: string;
eventID?: number;
+1 -1
View File
@@ -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());
};
+25 -2
View File
@@ -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", "browse"];
const HANDLED_ACTIONS: OpenHandsEventType[] = [
"run",
"run_ipython",
"write",
"read",
"browse",
];
function getRiskText(risk: ActionSecurityRisk) {
switch (risk) {
@@ -131,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) {
+17
View File
@@ -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;
+1
View File
@@ -14,6 +14,7 @@ export default {
'root-secondary': '#262626',
'hyperlink': '#007AFF',
'danger': '#EF3744',
'success': '#4CAF50',
},
},
},
+24 -1
View File
@@ -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>
+7 -1
View File
@@ -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
+1 -1
View File
@@ -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
+18 -9
View File
@@ -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
@@ -509,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 (
+1
View File
@@ -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
-8
View File
@@ -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'}
+8
View File
@@ -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}'
+3
View File
@@ -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)
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+1
View File
@@ -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)
+2
View File
@@ -8,6 +8,7 @@ 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.
@@ -15,6 +16,7 @@ class RuntimeBuilder(abc.ABC):
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").
+1 -1
View File
@@ -28,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.
+7 -1
View File
@@ -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(
+12 -1
View File
@@ -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}')
+6
View File
@@ -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
+4 -4
View File
@@ -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:
Generated
+50 -16
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -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"
@@ -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"
@@ -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
View File
@@ -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"
+27
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
from openhands.events.observation import CmdOutputObservation
from openhands.events.serialization import event_to_dict
def test_command_output_success_serialization():
# Test successful command
obs = CmdOutputObservation(
command_id=1, command='ls', content='file1.txt\nfile2.txt', exit_code=0
)
serialized = event_to_dict(obs)
assert serialized['success'] is True
# Test failed command
obs = CmdOutputObservation(
command_id=2, command='ls', content='No such file or directory', exit_code=1
)
serialized = event_to_dict(obs)
assert serialized['success'] is False
+19 -32
View File
@@ -40,36 +40,23 @@ def serialization_deserialization(
# Additional tests for various observation subclasses can be included here
def test_observation_event_props_serialization_deserialization():
original_observation_dict = {
'id': 42,
'source': 'agent',
'timestamp': '2021-08-01T12:00:00',
'observation': 'run',
'message': 'Command `ls -l` executed with exit code 0.',
'extras': {
'exit_code': 0,
'command': 'ls -l',
'command_id': 3,
'hidden': False,
'interpreter_details': '',
},
'content': 'foo.txt',
}
serialization_deserialization(original_observation_dict, CmdOutputObservation)
def test_success_field_serialization():
# Test success=True
obs = CmdOutputObservation(
content='Command succeeded',
exit_code=0,
command='ls -l',
command_id=3,
)
serialized = event_to_dict(obs)
assert serialized['success'] is True
def test_command_output_observation_serialization_deserialization():
original_observation_dict = {
'observation': 'run',
'extras': {
'exit_code': 0,
'command': 'ls -l',
'command_id': 3,
'hidden': False,
'interpreter_details': '',
},
'message': 'Command `ls -l` executed with exit code 0.',
'content': 'foo.txt',
}
serialization_deserialization(original_observation_dict, CmdOutputObservation)
# Test success=False
obs = CmdOutputObservation(
content='No such file or directory',
exit_code=1,
command='ls -l',
command_id=3,
)
serialized = event_to_dict(obs)
assert serialized['success'] is False
+3
View File
@@ -239,6 +239,7 @@ def test_build_runtime_image_from_scratch():
f'{get_runtime_image_repo()}:{OH_VERSION}_mock-versioned-tag',
],
platform=None,
extra_build_args=None,
)
assert (
image_name
@@ -333,6 +334,7 @@ def test_build_runtime_image_exact_hash_not_exist_and_lock_exist():
# VERSION tag will NOT be included except from scratch
],
platform=None,
extra_build_args=None,
)
mock_prep_build_folder.assert_called_once_with(
ANY,
@@ -391,6 +393,7 @@ def test_build_runtime_image_exact_hash_not_exist_and_lock_not_exist_and_version
# VERSION tag will NOT be included except from scratch
],
platform=None,
extra_build_args=None,
)
mock_prep_build_folder.assert_called_once_with(
ANY,
+201 -88
View File
@@ -51,24 +51,48 @@ def add_events(event_stream: EventStream, data: list[tuple[Event, EventSource]])
def test_msg(temp_dir: str):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
raise "Disallow ABC [risk=medium]" if:
(msg: Message)
"ABC" in msg.content
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(MessageAction('AB!'), EventSource.AGENT),
(MessageAction('Hello world!'), EventSource.USER),
(MessageAction('ABC!'), EventSource.AGENT),
mock_container = MagicMock()
mock_container.status = 'running'
mock_container.attrs = {
'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': 34567}]}}
}
mock_docker = MagicMock()
mock_docker.from_env().containers.list.return_value = [mock_container]
mock_requests = MagicMock()
mock_requests.get().json.return_value = {'id': 'mock-session-id'}
mock_requests.post().json.side_effect = [
{'monitor_id': 'mock-monitor-id'},
[], # First check
[], # Second check
[], # Third check
[
'PolicyViolation(Disallow ABC [risk=medium], ranges=[<2 ranges>])'
], # Fourth check
]
add_events(event_stream, data)
for i in range(3):
assert data[i][0].security_risk == ActionSecurityRisk.LOW
assert data[3][0].security_risk == ActionSecurityRisk.MEDIUM
with (
patch(f'{InvariantAnalyzer.__module__}.docker', mock_docker),
patch(f'{InvariantClient.__module__}.requests', mock_requests),
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
raise "Disallow ABC [risk=medium]" if:
(msg: Message)
"ABC" in msg.content
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(MessageAction('AB!'), EventSource.AGENT),
(MessageAction('Hello world!'), EventSource.USER),
(MessageAction('ABC!'), EventSource.AGENT),
]
add_events(event_stream, data)
for i in range(3):
assert data[i][0].security_risk == ActionSecurityRisk.LOW
assert data[3][0].security_risk == ActionSecurityRisk.MEDIUM
@pytest.mark.parametrize(
@@ -76,22 +100,44 @@ def test_msg(temp_dir: str):
[('rm -rf root_dir', ActionSecurityRisk.MEDIUM), ['ls', ActionSecurityRisk.LOW]],
)
def test_cmd(cmd, expected_risk, temp_dir: str):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
raise "Disallow rm -rf [risk=medium]" if:
(call: ToolCall)
call is tool:run
match("rm -rf", call.function.arguments.command)
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(CmdRunAction(cmd), EventSource.USER),
mock_container = MagicMock()
mock_container.status = 'running'
mock_container.attrs = {
'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': 34567}]}}
}
mock_docker = MagicMock()
mock_docker.from_env().containers.list.return_value = [mock_container]
mock_requests = MagicMock()
mock_requests.get().json.return_value = {'id': 'mock-session-id'}
mock_requests.post().json.side_effect = [
{'monitor_id': 'mock-monitor-id'},
[], # First check
['PolicyViolation(Disallow rm -rf [risk=medium], ranges=[<2 ranges>])']
if expected_risk == ActionSecurityRisk.MEDIUM
else [], # Second check
]
add_events(event_stream, data)
assert data[0][0].security_risk == ActionSecurityRisk.LOW
assert data[1][0].security_risk == expected_risk
with (
patch(f'{InvariantAnalyzer.__module__}.docker', mock_docker),
patch(f'{InvariantClient.__module__}.requests', mock_requests),
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
raise "Disallow rm -rf [risk=medium]" if:
(call: ToolCall)
call is tool:run
match("rm -rf", call.function.arguments.command)
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(CmdRunAction(cmd), EventSource.USER),
]
add_events(event_stream, data)
assert data[0][0].security_risk == ActionSecurityRisk.LOW
assert data[1][0].security_risk == expected_risk
@pytest.mark.parametrize(
@@ -102,26 +148,49 @@ def test_cmd(cmd, expected_risk, temp_dir: str):
],
)
def test_leak_secrets(code, expected_risk, temp_dir: str):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
from invariant.detectors import secrets
mock_container = MagicMock()
mock_container.status = 'running'
mock_container.attrs = {
'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': 34567}]}}
}
mock_docker = MagicMock()
mock_docker.from_env().containers.list.return_value = [mock_container]
raise "Disallow writing secrets [risk=medium]" if:
(call: ToolCall)
call is tool:run_ipython
any(secrets(call.function.arguments.code))
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(IPythonRunCellAction(code), EventSource.AGENT),
(IPythonRunCellAction('hello'), EventSource.AGENT),
mock_requests = MagicMock()
mock_requests.get().json.return_value = {'id': 'mock-session-id'}
mock_requests.post().json.side_effect = [
{'monitor_id': 'mock-monitor-id'},
[], # First check
['PolicyViolation(Disallow writing secrets [risk=medium], ranges=[<2 ranges>])']
if expected_risk == ActionSecurityRisk.MEDIUM
else [], # Second check
[], # Third check
]
add_events(event_stream, data)
assert data[0][0].security_risk == ActionSecurityRisk.LOW
assert data[1][0].security_risk == expected_risk
assert data[2][0].security_risk == ActionSecurityRisk.LOW
with (
patch(f'{InvariantAnalyzer.__module__}.docker', mock_docker),
patch(f'{InvariantClient.__module__}.requests', mock_requests),
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
policy = """
from invariant.detectors import secrets
raise "Disallow writing secrets [risk=medium]" if:
(call: ToolCall)
call is tool:run_ipython
any(secrets(call.function.arguments.code))
"""
InvariantAnalyzer(event_stream, policy)
data = [
(MessageAction('Hello world!'), EventSource.USER),
(IPythonRunCellAction(code), EventSource.AGENT),
(IPythonRunCellAction('hello'), EventSource.AGENT),
]
add_events(event_stream, data)
assert data[0][0].security_risk == ActionSecurityRisk.LOW
assert data[1][0].security_risk == expected_risk
assert data[2][0].security_risk == ActionSecurityRisk.LOW
def test_unsafe_python_code(temp_dir: str):
@@ -458,26 +527,48 @@ def default_config():
def test_check_usertask(
mock_litellm_completion, usertask, is_appropriate, default_config, temp_dir: str
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
analyzer = InvariantAnalyzer(event_stream)
mock_response = {'choices': [{'message': {'content': is_appropriate}}]}
mock_litellm_completion.return_value = mock_response
analyzer.guardrail_llm = LLM(config=default_config)
analyzer.check_browsing_alignment = True
data = [
(MessageAction(usertask), EventSource.USER),
]
add_events(event_stream, data)
event_list = list(event_stream.get_events())
mock_container = MagicMock()
mock_container.status = 'running'
mock_container.attrs = {
'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': 34567}]}}
}
mock_docker = MagicMock()
mock_docker.from_env().containers.list.return_value = [mock_container]
if is_appropriate == 'No':
assert len(event_list) == 2
assert type(event_list[0]) == MessageAction
assert type(event_list[1]) == ChangeAgentStateAction
elif is_appropriate == 'Yes':
assert len(event_list) == 1
assert type(event_list[0]) == MessageAction
mock_requests = MagicMock()
mock_requests.get().json.return_value = {'id': 'mock-session-id'}
mock_requests.post().json.side_effect = [
{'monitor_id': 'mock-monitor-id'},
[],
[
'PolicyViolation(Vulnerability in python code [risk=medium], ranges=[<2 ranges>])'
],
]
with (
patch(f'{InvariantAnalyzer.__module__}.docker', mock_docker),
patch(f'{InvariantClient.__module__}.requests', mock_requests),
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
analyzer = InvariantAnalyzer(event_stream)
mock_response = {'choices': [{'message': {'content': is_appropriate}}]}
mock_litellm_completion.return_value = mock_response
analyzer.guardrail_llm = LLM(config=default_config)
analyzer.check_browsing_alignment = True
data = [
(MessageAction(usertask), EventSource.USER),
]
add_events(event_stream, data)
event_list = list(event_stream.get_events())
if is_appropriate == 'No':
assert len(event_list) == 2
assert type(event_list[0]) == MessageAction
assert type(event_list[1]) == ChangeAgentStateAction
elif is_appropriate == 'Yes':
assert len(event_list) == 1
assert type(event_list[0]) == MessageAction
@pytest.mark.parametrize(
@@ -491,23 +582,45 @@ def test_check_usertask(
def test_check_fillaction(
mock_litellm_completion, fillaction, is_harmful, default_config, temp_dir: str
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
analyzer = InvariantAnalyzer(event_stream)
mock_response = {'choices': [{'message': {'content': is_harmful}}]}
mock_litellm_completion.return_value = mock_response
analyzer.guardrail_llm = LLM(config=default_config)
analyzer.check_browsing_alignment = True
data = [
(BrowseInteractiveAction(browser_actions=fillaction), EventSource.AGENT),
]
add_events(event_stream, data)
event_list = list(event_stream.get_events())
mock_container = MagicMock()
mock_container.status = 'running'
mock_container.attrs = {
'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': 34567}]}}
}
mock_docker = MagicMock()
mock_docker.from_env().containers.list.return_value = [mock_container]
if is_harmful == 'Yes':
assert len(event_list) == 2
assert type(event_list[0]) == BrowseInteractiveAction
assert type(event_list[1]) == ChangeAgentStateAction
elif is_harmful == 'No':
assert len(event_list) == 1
assert type(event_list[0]) == BrowseInteractiveAction
mock_requests = MagicMock()
mock_requests.get().json.return_value = {'id': 'mock-session-id'}
mock_requests.post().json.side_effect = [
{'monitor_id': 'mock-monitor-id'},
[],
[
'PolicyViolation(Vulnerability in python code [risk=medium], ranges=[<2 ranges>])'
],
]
with (
patch(f'{InvariantAnalyzer.__module__}.docker', mock_docker),
patch(f'{InvariantClient.__module__}.requests', mock_requests),
):
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('main', file_store)
analyzer = InvariantAnalyzer(event_stream)
mock_response = {'choices': [{'message': {'content': is_harmful}}]}
mock_litellm_completion.return_value = mock_response
analyzer.guardrail_llm = LLM(config=default_config)
analyzer.check_browsing_alignment = True
data = [
(BrowseInteractiveAction(browser_actions=fillaction), EventSource.AGENT),
]
add_events(event_stream, data)
event_list = list(event_stream.get_events())
if is_harmful == 'Yes':
assert len(event_list) == 2
assert type(event_list[0]) == BrowseInteractiveAction
assert type(event_list[1]) == ChangeAgentStateAction
elif is_harmful == 'No':
assert len(event_list) == 1
assert type(event_list[0]) == BrowseInteractiveAction