mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
45 Commits
0.28.0
...
fix-termin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d67c4c5848 | ||
|
|
36b378c561 | ||
|
|
ec763f8105 | ||
|
|
165c0cc42e | ||
|
|
1b4f15235e | ||
|
|
303b7ab180 | ||
|
|
78d185b102 | ||
|
|
300bfbdf2d | ||
|
|
e2f414bf26 | ||
|
|
3b955dd9d5 | ||
|
|
f1eb1f59c3 | ||
|
|
e1f6929d98 | ||
|
|
2a7f926591 | ||
|
|
b8daab721d | ||
|
|
b3cac69121 | ||
|
|
49a29c19cb | ||
|
|
7084b0238c | ||
|
|
38e866cde4 | ||
|
|
6ec4bc74bf | ||
|
|
b36deca265 | ||
|
|
09d73d96c8 | ||
|
|
966c11f205 | ||
|
|
5128377baa | ||
|
|
924acb182b | ||
|
|
8043612420 | ||
|
|
637a1d5c17 | ||
|
|
5e521a4a6e | ||
|
|
2cb5b91300 | ||
|
|
d6e601ea2e | ||
|
|
ac680e7688 | ||
|
|
4b04f09035 | ||
|
|
e99f503e0f | ||
|
|
f51eb93d3e | ||
|
|
7c2a98d1ce | ||
|
|
dc9489ddcd | ||
|
|
83851c398d | ||
|
|
a4908f9a75 | ||
|
|
366fd7ab8a | ||
|
|
318fcbcfc7 | ||
|
|
521492f8f9 | ||
|
|
71f6b0b4a9 | ||
|
|
b146b63380 | ||
|
|
22064d5555 | ||
|
|
08735a9b72 | ||
|
|
54f982589a |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -10,12 +10,6 @@ updates:
|
||||
pre-commit:
|
||||
patterns:
|
||||
- "pre-commit"
|
||||
llama:
|
||||
patterns:
|
||||
- "llama*"
|
||||
chromadb:
|
||||
patterns:
|
||||
- "chromadb"
|
||||
browsergym:
|
||||
patterns:
|
||||
- "browsergym*"
|
||||
|
||||
2
.github/workflows/dummy-agent-test.yml
vendored
2
.github/workflows/dummy-agent-test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: '3.12'
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
run: poetry install --without evaluation
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run tests
|
||||
|
||||
72
.github/workflows/integration-runner.yml
vendored
72
.github/workflows/integration-runner.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
run: poetry install --without evaluation
|
||||
|
||||
- name: Configure config.toml for testing with Haiku
|
||||
env:
|
||||
@@ -117,68 +117,6 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Run DelegatorAgent tests for Haiku, limited to t01 and t02
|
||||
- name: Wait a little bit (again)
|
||||
run: sleep 5
|
||||
|
||||
- name: Configure config.toml for testing DelegatorAgent (Haiku)
|
||||
env:
|
||||
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
MAX_ITERATIONS: 30
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"$LLM_MODEL\"" >> config.toml
|
||||
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
|
||||
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
|
||||
- name: Run integration test evaluation for DelegatorAgent (Haiku)
|
||||
env:
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME: True
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_haiku_run'
|
||||
|
||||
# Find and export the delegator test results
|
||||
REPORT_FILE_DELEGATOR_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/*haiku*_maxiter_30_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE_DELEGATOR_HAIKU: $REPORT_FILE_DELEGATOR_HAIKU"
|
||||
echo "INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE_DELEGATOR_HAIKU >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Run DelegatorAgent tests for DeepSeek, limited to t01 and t02
|
||||
- name: Wait a little bit (again)
|
||||
run: sleep 5
|
||||
|
||||
- name: Configure config.toml for testing DelegatorAgent (DeepSeek)
|
||||
env:
|
||||
LLM_MODEL: "litellm_proxy/deepseek-chat"
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
MAX_ITERATIONS: 30
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"$LLM_MODEL\"" >> config.toml
|
||||
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
|
||||
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
- name: Run integration test evaluation for DelegatorAgent (DeepSeek)
|
||||
env:
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME: True
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_deepseek_run'
|
||||
|
||||
# Find and export the delegator test results
|
||||
REPORT_FILE_DELEGATOR_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/deepseek*_maxiter_30_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE_DELEGATOR_DEEPSEEK: $REPORT_FILE_DELEGATOR_DEEPSEEK"
|
||||
echo "INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE_DELEGATOR_DEEPSEEK >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
# -------------------------------------------------------------
|
||||
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
|
||||
- name: Wait a little bit (again)
|
||||
run: sleep 5
|
||||
@@ -213,7 +151,7 @@ jobs:
|
||||
run: |
|
||||
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
|
||||
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
|
||||
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/DelegatorAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
|
||||
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
|
||||
|
||||
- name: Upload evaluation results as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -254,12 +192,6 @@ jobs:
|
||||
**Integration Tests Report (DeepSeek)**
|
||||
DeepSeek LLM Test Results:
|
||||
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
|
||||
---
|
||||
**Integration Tests Report Delegator (Haiku)**
|
||||
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU }}
|
||||
---
|
||||
**Integration Tests Report Delegator (DeepSeek)**
|
||||
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK }}
|
||||
---
|
||||
**Integration Tests Report VisualBrowsing (DeepSeek)**
|
||||
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}
|
||||
|
||||
4
.github/workflows/py-unit-tests.yml
vendored
4
.github/workflows/py-unit-tests.yml
vendored
@@ -44,11 +44,11 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
run: poetry install --without evaluation
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_long_term_memory.py
|
||||
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
|
||||
31
Makefile
31
Makefile
@@ -133,7 +133,7 @@ install-python-dependencies:
|
||||
export HNSWLIB_NO_NATIVE=1; \
|
||||
poetry run pip install chroma-hnswlib; \
|
||||
fi
|
||||
@poetry install --without llama-index
|
||||
@poetry install
|
||||
@if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
|
||||
poetry run pip install playwright; \
|
||||
@@ -265,35 +265,6 @@ setup-config-prompts:
|
||||
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
|
||||
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
|
||||
|
||||
@echo "Enter your LLM Embedding Model"; \
|
||||
echo "Choices are:"; \
|
||||
echo " - openai"; \
|
||||
echo " - azureopenai"; \
|
||||
echo " - Embeddings available only with OllamaEmbedding:"; \
|
||||
echo " - llama2"; \
|
||||
echo " - mxbai-embed-large"; \
|
||||
echo " - nomic-embed-text"; \
|
||||
echo " - all-minilm"; \
|
||||
echo " - stable-code"; \
|
||||
echo " - bge-m3"; \
|
||||
echo " - bge-large"; \
|
||||
echo " - paraphrase-multilingual"; \
|
||||
echo " - snowflake-arctic-embed"; \
|
||||
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
|
||||
read -p "> " llm_embedding_model; \
|
||||
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
|
||||
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
|
||||
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
|
||||
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
|
||||
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
|
||||
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
|
||||
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure API Version: " llm_api_version; \
|
||||
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
|
||||
fi
|
||||
|
||||
|
||||
# Develop in container
|
||||
docker-dev:
|
||||
|
||||
@@ -132,15 +132,6 @@ api_key = ""
|
||||
# Custom LLM provider
|
||||
#custom_llm_provider = ""
|
||||
|
||||
# Embedding API base URL
|
||||
#embedding_base_url = ""
|
||||
|
||||
# Embedding deployment name
|
||||
#embedding_deployment_name = ""
|
||||
|
||||
# Embedding model to use
|
||||
embedding_model = "local"
|
||||
|
||||
# Maximum number of characters in an observation's content
|
||||
#max_message_chars = 10000
|
||||
|
||||
@@ -227,9 +218,6 @@ codeact_enable_llm_editor = false
|
||||
# whether the IPython tool is enabled
|
||||
codeact_enable_jupyter = true
|
||||
|
||||
# Name of the micro agent to use for this agent
|
||||
#micro_agent_name = ""
|
||||
|
||||
# Memory enabled
|
||||
#memory_enabled = false
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN apt-get update -y \
|
||||
|
||||
COPY ./pyproject.toml ./poetry.lock ./
|
||||
RUN touch README.md
|
||||
RUN export POETRY_CACHE_DIR && poetry install --without evaluation,llama-index --no-root && rm -rf $POETRY_CACHE_DIR
|
||||
RUN export POETRY_CACHE_DIR && poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
|
||||
|
||||
FROM python:3.12.3-slim AS openhands-app
|
||||
|
||||
|
||||
@@ -333,12 +333,6 @@ Pour les configurations de développement, vous pouvez également définir des c
|
||||
|
||||
Les options de configuration de l'agent sont définies dans les sections `[agent]` et `[agent.<agent_name>]` du fichier `config.toml`.
|
||||
|
||||
**Configuration du micro-agent**
|
||||
- `micro_agent_name`
|
||||
- Type : `str`
|
||||
- Valeur par défaut : `""`
|
||||
- Description : Nom du micro-agent à utiliser pour cet agent
|
||||
|
||||
**Configuration de la mémoire**
|
||||
- `memory_enabled`
|
||||
- Type : `bool`
|
||||
|
||||
@@ -328,12 +328,6 @@ LLM(大语言模型)配置选项在 `config.toml` 文件的 `[llm]` 部分中定
|
||||
|
||||
Agent 配置选项在 `config.toml` 文件的 `[agent]` 和 `[agent.<agent_name>]` 部分中定义。
|
||||
|
||||
**Microagent 配置**
|
||||
- `micro_agent_name`
|
||||
- 类型: `str`
|
||||
- 默认值: `""`
|
||||
- 描述: 用于此 agent 的 micro agent 名称
|
||||
|
||||
**内存配置**
|
||||
- `memory_enabled`
|
||||
- 类型: `bool`
|
||||
|
||||
@@ -4,18 +4,30 @@ The GitHub Resolver automates code fixes and provides intelligent assistance for
|
||||
|
||||
## Setup
|
||||
|
||||
The Cloud Github Resolver is available automatically when you
|
||||
The Cloud GitHub Resolver is available automatically when you
|
||||
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repositories).
|
||||
|
||||
## Usage
|
||||
|
||||
After granting OpenHands Cloud repository access, you can use the Cloud GitHub Resolver on the issues and pull requests
|
||||
on the repository.
|
||||
|
||||
### Issues
|
||||
|
||||
On your repository, label an issue with `openhands`. OpenHands will attempt to fix the issue.
|
||||
On your repository, label an issue with `openhands`. OpenHands will:
|
||||
1. Comment on the issue to let you know it is working on it.
|
||||
- You can click on the link to track the progress on OpenHands Cloud.
|
||||
2. Open a pull request if it determines that the issue has been successfully resolved.
|
||||
3. Comment on the issue with a summary of the performed tasks and a link to the pull request.
|
||||
|
||||
|
||||
### Pull Requests
|
||||
|
||||
In order to get OpenHands to work on pull requests, use `@openhands` in top level or single inline comments to:
|
||||
To get OpenHands to work on pull requests, use `@openhands` in top level or inline comments to:
|
||||
- Ask questions
|
||||
- Request updates
|
||||
- Get code explanations
|
||||
|
||||
OpenHands will:
|
||||
1. Comment on the PR to let you know it is working on it.
|
||||
2. Perform the task.
|
||||
|
||||
@@ -12,14 +12,16 @@ instructions on how to access it.
|
||||
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
|
||||
1. After reading and accepting the terms of service, click `Connect to GitHub`.
|
||||
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands by All Hands AI`.
|
||||
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands AI`.
|
||||
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
|
||||
you can click the `Learn more` link on the GitHub authorize page.
|
||||
|
||||
## Adding Repositories
|
||||
## Repository Access
|
||||
|
||||
### Adding Repository Access
|
||||
|
||||
You can grant OpenHands specific repository access:
|
||||
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
|
||||
1. Click the `Select a GitHub project` dropdown, select `Add more repositories...`.
|
||||
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
|
||||
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
|
||||
- Actions: Read and write
|
||||
@@ -34,6 +36,10 @@ You can grant OpenHands specific repository access:
|
||||
- Repository access for a user is granted based on:
|
||||
- Granted permission for the repository.
|
||||
- User's GitHub permissions (owner/collaborator).
|
||||
3. Click on `Install & Authorize`.
|
||||
|
||||
You can manage repository access any time by following the above workflow or visiting the Settings page and selecting
|
||||
`Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
### Modifying Repository Access
|
||||
|
||||
You can modify repository access at any time by:
|
||||
* Using the same `Select a GitHub project > Add more repositories` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
|
||||
@@ -197,21 +197,6 @@ For development setups, you can also define custom named LLM configurations. See
|
||||
- Default: `""`
|
||||
- Description: Custom LLM provider
|
||||
|
||||
### Embeddings
|
||||
- `embedding_base_url`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: Embedding API base URL
|
||||
|
||||
- `embedding_deployment_name`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: Embedding deployment name
|
||||
|
||||
- `embedding_model`
|
||||
- Type: `str`
|
||||
- Default: `"local"`
|
||||
- Description: Embedding model to use
|
||||
|
||||
### Message Handling
|
||||
- `max_message_chars`
|
||||
@@ -296,23 +281,6 @@ For development setups, you can also define custom named LLM configurations. See
|
||||
|
||||
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
|
||||
|
||||
### Microagent Configuration
|
||||
- `micro_agent_name`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: Name of the micro agent to use for this agent
|
||||
|
||||
### Memory Configuration
|
||||
- `memory_enabled`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
- Description: Whether long-term memory (embeddings) is enabled
|
||||
|
||||
- `memory_max_threads`
|
||||
- Type: `int`
|
||||
- Default: `3`
|
||||
- Description: The maximum number of threads indexing at the same time for embeddings
|
||||
|
||||
### LLM Configuration
|
||||
- `llm_config`
|
||||
- Type: `str`
|
||||
@@ -387,7 +355,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
|
||||
|
||||
- `runtime_binding_address`
|
||||
- Type: `str`
|
||||
- Default: `127.0.0.1`
|
||||
- Default: `0.0.0.0`
|
||||
- Description: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
|
||||
|
||||
### Linting and Plugins
|
||||
|
||||
@@ -36,7 +36,7 @@ At this time, we will follow the following release process:
|
||||
1. All people who contributed public feedback will receive an email describing the data release and being given an opportunity to opt out.
|
||||
2. The person or people in charge of the data release will perform quality control of the data, removing low-quality feedback,
|
||||
removing email submitter email addresses, and attempting to remove any sensitive information.
|
||||
3. The data will be released publicly under the MIT license through commonly used sites such as github or Hugging Face.
|
||||
3. The data will be released publicly under the MIT license through commonly used sites such as GitHub or Hugging Face.
|
||||
|
||||
### What if I want my data deleted?
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You can provide custom directions for OpenHands by following the [README for the
|
||||
|
||||
### Custom configurations
|
||||
|
||||
Github resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
|
||||
GitHub resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
|
||||
The customization options you can set are:
|
||||
|
||||
| **Attribute name** | **Type** | **Purpose** | **Example** |
|
||||
|
||||
@@ -76,7 +76,7 @@ You'll find OpenHands running at http://localhost:3000!
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
|
||||
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
|
||||
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
|
||||
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
or run it on tagged issues with [a GitHub action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -31,17 +31,11 @@ You will need your ChatGPT deployment name which can be found on the deployments
|
||||
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
|
||||
- `API Key` to your Azure API key
|
||||
|
||||
## Embeddings
|
||||
|
||||
OpenHands uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/).
|
||||
|
||||
### Azure OpenAI Configuration
|
||||
|
||||
When running OpenHands, set the following environment variables using `-e` in the
|
||||
When running OpenHands, set the following environment variable using `-e` in the
|
||||
[docker run command](/modules/usage/installation#start-the-app):
|
||||
|
||||
```
|
||||
LLM_EMBEDDING_MODEL="azureopenai"
|
||||
LLM_EMBEDDING_DEPLOYMENT_NAME="<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
```
|
||||
|
||||
@@ -10,6 +10,9 @@ We also support "remote" runtimes, which are typically managed by third-parties.
|
||||
They can make setup a bit simpler and more scalable, especially
|
||||
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
|
||||
|
||||
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
|
||||
which can be useful in controlled environments like CI pipelines.
|
||||
|
||||
## Docker Runtime
|
||||
This is the default Runtime that's used when you start OpenHands. You might notice
|
||||
some flags being passed to `docker run` that make this possible:
|
||||
@@ -56,11 +59,12 @@ any files that are mounted into its workspace.
|
||||
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
|
||||
but seems to work well on most systems.
|
||||
|
||||
## All Hands Runtime
|
||||
The All Hands Runtime is currently in beta. You can request access by joining
|
||||
the #remote-runtime-limited-beta channel on Slack ([see the README](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community) for an invite).
|
||||
## OpenHands Remote Runtime
|
||||
|
||||
To use the All Hands Runtime, set the following environment variables when
|
||||
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud.
|
||||
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
|
||||
|
||||
To use the OpenHands Remote Runtime, set the following environment variables when
|
||||
starting OpenHands:
|
||||
|
||||
```bash
|
||||
@@ -117,3 +121,66 @@ bash -i <(curl -sL https://get.daytona.io/openhands)
|
||||
Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)
|
||||
|
||||
## Local Runtime
|
||||
|
||||
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
|
||||
|
||||
:::caution
|
||||
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
|
||||
:::
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before using the Local Runtime, ensure you have the following dependencies installed:
|
||||
|
||||
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
2. tmux is available on your system.
|
||||
|
||||
### Configuration
|
||||
|
||||
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
|
||||
|
||||
- Via environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export RUNTIME=local
|
||||
|
||||
# Optional but recommended
|
||||
export WORKSPACE_BASE=/path/to/your/workspace
|
||||
```
|
||||
|
||||
- Via `config.toml`:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
runtime = "local"
|
||||
workspace_base = "/path/to/your/workspace"
|
||||
```
|
||||
|
||||
If `WORKSPACE_BASE` is not set, the runtime will create a temporary directory for the agent to work in.
|
||||
|
||||
### Example Usage
|
||||
|
||||
Here's an example of how to start OpenHands with the Local Runtime in Headless Mode:
|
||||
|
||||
```bash
|
||||
# Set the runtime type to local
|
||||
export RUNTIME=local
|
||||
|
||||
# Optionally set a workspace directory
|
||||
export WORKSPACE_BASE=/path/to/your/project
|
||||
|
||||
# Start OpenHands
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
The Local Runtime is particularly useful for:
|
||||
|
||||
- CI/CD pipelines where Docker is not available.
|
||||
- Testing and development of OpenHands itself.
|
||||
- Environments where container usage is restricted.
|
||||
- Scenarios where direct file system access is required.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Commit0 Evaluation with OpenHands
|
||||
|
||||
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](TBD)).
|
||||
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](https://arxiv.org/abs/2412.01769v1)).
|
||||
|
||||
The evaluation consists of three steps:
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
@@ -91,12 +93,22 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
|
||||
return config
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConditionalImports:
|
||||
"""We instantiate the values in this dataclass differently if we're evaluating SWE-bench or SWE-Gym."""
|
||||
|
||||
get_eval_report: Callable
|
||||
APPLY_PATCH_FAIL: str
|
||||
APPLY_PATCH_PASS: str
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
log_dir: str | None = None,
|
||||
runtime_failure_count: int = 0,
|
||||
conditional_imports: ConditionalImports | None = None,
|
||||
) -> EvalOutput:
|
||||
"""
|
||||
Evaluate agent performance on a SWE-bench problem instance.
|
||||
@@ -108,9 +120,18 @@ def process_instance(
|
||||
log_dir (str | None, default=None): Path to directory where log files will be written. Must
|
||||
be provided if `reset_logger` is set.
|
||||
|
||||
conditional_imports: A dataclass containing values that are imported differently based on
|
||||
whether we're evaluating SWE-bench or SWE-Gym.
|
||||
|
||||
Raises:
|
||||
AssertionError: if the `reset_logger` flag is set without a provided log directory.
|
||||
|
||||
AssertionError: if `conditional_imports` is not provided.
|
||||
"""
|
||||
assert (
|
||||
conditional_imports is not None
|
||||
), 'conditional_imports must be provided to run process_instance using multiprocessing'
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
assert (
|
||||
@@ -124,7 +145,7 @@ def process_instance(
|
||||
config = get_config(metadata, instance)
|
||||
instance_id = instance.instance_id
|
||||
model_patch = instance['model_patch']
|
||||
test_spec: TestSpec = instance['test_spec']
|
||||
test_spec = instance['test_spec']
|
||||
logger.info(f'Starting evaluation for instance {instance_id}.')
|
||||
|
||||
if 'test_result' not in instance.keys():
|
||||
@@ -196,7 +217,9 @@ def process_instance(
|
||||
instance['test_result']['apply_patch_output'] = apply_patch_output
|
||||
|
||||
if 'APPLY_PATCH_FAIL' in apply_patch_output:
|
||||
logger.info(f'[{instance_id}] {APPLY_PATCH_FAIL}:\n{apply_patch_output}')
|
||||
logger.info(
|
||||
f'[{instance_id}] {conditional_imports.APPLY_PATCH_FAIL}:\n{apply_patch_output}'
|
||||
)
|
||||
instance['test_result']['report']['failed_apply_patch'] = True
|
||||
|
||||
return EvalOutput(
|
||||
@@ -205,7 +228,9 @@ def process_instance(
|
||||
metadata=metadata,
|
||||
)
|
||||
elif 'APPLY_PATCH_PASS' in apply_patch_output:
|
||||
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
|
||||
logger.info(
|
||||
f'[{instance_id}] {conditional_imports.APPLY_PATCH_PASS}:\n{apply_patch_output}'
|
||||
)
|
||||
|
||||
# Run eval script in background and save output to log file
|
||||
log_file = '/tmp/eval_output.log'
|
||||
@@ -271,7 +296,7 @@ def process_instance(
|
||||
with open(test_output_path, 'w') as f:
|
||||
f.write(test_output)
|
||||
try:
|
||||
_report = get_eval_report(
|
||||
_report = conditional_imports.get_eval_report(
|
||||
test_spec=test_spec,
|
||||
prediction={
|
||||
'model_patch': model_patch,
|
||||
@@ -345,7 +370,6 @@ if __name__ == '__main__':
|
||||
)
|
||||
from swegym.harness.test_spec import (
|
||||
SWEbenchInstance,
|
||||
TestSpec,
|
||||
make_test_spec,
|
||||
)
|
||||
from swegym.harness.utils import load_swebench_dataset
|
||||
@@ -357,7 +381,6 @@ if __name__ == '__main__':
|
||||
)
|
||||
from swebench.harness.test_spec.test_spec import (
|
||||
SWEbenchInstance,
|
||||
TestSpec,
|
||||
make_test_spec,
|
||||
)
|
||||
from swebench.harness.utils import load_swebench_dataset
|
||||
@@ -445,7 +468,15 @@ if __name__ == '__main__':
|
||||
# The evaluation harness constrains the signature of `process_instance_func` but we need to
|
||||
# pass extra information. Build a new function object to avoid issues with multiprocessing.
|
||||
process_instance_func = partial(
|
||||
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
|
||||
process_instance,
|
||||
log_dir=output_file.replace('.jsonl', '.logs'),
|
||||
# We have to explicitly pass these imports to the process_instance function, otherwise
|
||||
# they won't be available in the multiprocessing context.
|
||||
conditional_imports=ConditionalImports(
|
||||
get_eval_report=get_eval_report,
|
||||
APPLY_PATCH_FAIL=APPLY_PATCH_FAIL,
|
||||
APPLY_PATCH_PASS=APPLY_PATCH_PASS,
|
||||
),
|
||||
)
|
||||
|
||||
run_evaluation(
|
||||
|
||||
@@ -58,62 +58,89 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
|
||||
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
# Instruction based on Anthropic's official trajectory
|
||||
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
|
||||
instruction = (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:\n\n"
|
||||
f'<issue_description>\n'
|
||||
f'{instance.problem_statement}\n'
|
||||
'</issue_description>\n\n'
|
||||
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?\n'
|
||||
"I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
|
||||
"Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.\n"
|
||||
'Your task is to make the minimal changes to non-test files in the /workspace directory to ensure the <issue_description> is satisfied.\n'
|
||||
'Follow these steps to resolve the issue:\n'
|
||||
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
|
||||
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
|
||||
'3. Edit the sourcecode of the repo to resolve the issue\n'
|
||||
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'5. Think about edgecases, add comprehensive tests for them in your reproduce script, and run them to make sure your fix handles them as well\n'
|
||||
f'6. Once you are done with the initial implementation, please carefully re-read the problem description and check the difference between the current code and the base commit {instance["base_commit"]}. Do you think that the issue has been completely and comprehensively solved? Write tests to check the correctness of the solution, specifically focusing on tests that may point out any remaining problems that are not yet solved. Run all of the tests in the repo and check if any of them fail, and if they do fix the code. Repeat this process of carefully reading the problem description and current implementation, testing, and fixing any problems until you are confident that the current implementation is correct. Find and run any tests in the repo that are related to:\n'
|
||||
' - The issue you are fixing\n'
|
||||
' - The files you modified\n'
|
||||
' - The functions you changed\n'
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
)
|
||||
instruction = f"""
|
||||
<uploaded_files>
|
||||
/workspace/{workspace_dir_name}
|
||||
</uploaded_files>
|
||||
|
||||
I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:
|
||||
|
||||
<issue_description>
|
||||
{instance.problem_statement}
|
||||
</issue_description>
|
||||
|
||||
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
|
||||
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these steps to resolve the issue:
|
||||
|
||||
1. EXPLORATION: First, thoroughly explore the repository structure using tools like `find` and `grep`.
|
||||
- Identify all files mentioned in the problem statement
|
||||
- Locate where the issue occurs in the codebase
|
||||
- Understand the surrounding context and dependencies
|
||||
- Use `grep` to search for relevant functions, classes, or error messages
|
||||
|
||||
2. ANALYSIS: Based on your exploration, think carefully about the problem and propose 2-5 possible approaches to fix the issue.
|
||||
- Analyze the root cause of the problem
|
||||
- Consider trade-offs between different solutions
|
||||
- Select the most promising approach and explain your reasoning
|
||||
|
||||
3. TEST CREATION: Before implementing any fix, create a script to reproduce and verify the issue.
|
||||
- Look at existing test files in the repository to understand the test format/structure
|
||||
- Create a minimal reproduction script that demonstrates the issue
|
||||
- Run your script to confirm the error exists
|
||||
|
||||
4. IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
- Make minimal, focused changes to fix the issue
|
||||
|
||||
5. VERIFICATION: Test your implementation thoroughly.
|
||||
- Run your reproduction script to verify the fix works
|
||||
- Add edge cases to your test script to ensure comprehensive coverage
|
||||
- Run existing tests related to the modified code to ensure you haven't broken anything
|
||||
|
||||
6. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
|
||||
- Ensure you've fully addressed all requirements
|
||||
- Run any tests in the repository related to:
|
||||
* The issue you are fixing
|
||||
* The files you modified
|
||||
* The functions you changed
|
||||
- If any tests fail, revise your implementation until all tests pass
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
"""
|
||||
|
||||
if RUN_WITH_BROWSING:
|
||||
instruction += (
|
||||
'<IMPORTANT!>\n'
|
||||
'You SHOULD NEVER attempt to browse the web. '
|
||||
'</IMPORTANT!>\n'
|
||||
)
|
||||
instruction += """
|
||||
<IMPORTANT!>
|
||||
You SHOULD NEVER attempt to browse the web.
|
||||
</IMPORTANT!>
|
||||
"""
|
||||
return instruction
|
||||
|
||||
|
||||
# TODO: migrate all swe-bench docker to ghcr.io/openhands
|
||||
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
|
||||
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
|
||||
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
|
||||
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
def get_instance_docker_image(instance_id: str, official_image: bool = False) -> str:
|
||||
if official_image:
|
||||
# Official SWE-Bench image
|
||||
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
|
||||
docker_image_prefix = 'docker.io/swebench/'
|
||||
repo, name = instance_id.split('__')
|
||||
image_name = f'sweb.eval.x86_64.{repo}_1776_{name}:latest'
|
||||
logger.warning(f'Using official SWE-Bench image: {image_name}')
|
||||
else:
|
||||
# OpenHands version of the image
|
||||
docker_image_prefix = DEFAULT_DOCKER_IMAGE_PREFIX
|
||||
image_name = 'sweb.eval.x86_64.' + instance_id
|
||||
image_name = image_name.replace(
|
||||
'__', '_s_'
|
||||
) # to comply with docker image naming convention
|
||||
return (DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + image_name).lower()
|
||||
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
|
||||
|
||||
|
||||
def get_config(
|
||||
|
||||
@@ -4,8 +4,10 @@ import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
import * as featureFlags from "#/utils/feature-flags";
|
||||
|
||||
describe("PaymentForm", () => {
|
||||
const billingSettingsSpy = vi.spyOn(featureFlags, "BILLING_SETTINGS");
|
||||
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
|
||||
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
@@ -26,6 +28,7 @@ describe("PaymentForm", () => {
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
billingSettingsSpy.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -72,8 +72,10 @@ describe("useTerminal", () => {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
|
||||
// Input commands should be displayed
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith("echo hello");
|
||||
// Output commands should be displayed
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith("hello");
|
||||
});
|
||||
|
||||
it("should hide secrets in the terminal", () => {
|
||||
@@ -97,13 +99,31 @@ describe("useTerminal", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// BUG: `vi.clearAllMocks()` does not clear the number of calls
|
||||
// therefore, we need to assume the order of the calls based
|
||||
// on the test order
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
// Input command should be displayed with secrets masked
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith(
|
||||
`export GITHUB_TOKEN=${"*".repeat(10)},${"*".repeat(10)},${"*".repeat(10)}`,
|
||||
);
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(4, "*".repeat(10));
|
||||
|
||||
// Output command should be displayed with secrets masked
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith("*".repeat(10));
|
||||
});
|
||||
|
||||
it("should prevent duplicate command display", () => {
|
||||
const inputCommand = "ls -la";
|
||||
const commands: Command[] = [
|
||||
{ content: inputCommand, type: "input" },
|
||||
{ content: `${inputCommand}\nfile1.txt\nfile2.txt`, type: "output" },
|
||||
];
|
||||
|
||||
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
// Input command should be displayed
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith(inputCommand);
|
||||
|
||||
// Output should not be displayed since it starts with the input command
|
||||
// This prevents the duplicate display of the command
|
||||
expect(mockTerminal.writeln).not.toHaveBeenCalledWith(`${inputCommand}\nfile1.txt\nfile2.txt`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,13 @@ import { createRoutesStub } from "react-router";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import App from "#/routes/_oh.app/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as CustomToast from "#/utils/custom-toast-handlers";
|
||||
|
||||
describe("App", () => {
|
||||
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
|
||||
|
||||
const RouteStub = createRoutesStub([
|
||||
{ Component: App, path: "/conversation/:conversationId" },
|
||||
]);
|
||||
@@ -34,26 +36,19 @@ describe("App", () => {
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it(
|
||||
"should call endSession if the user does not have permission to view conversation",
|
||||
async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
it("should call endSession if the user does not have permission to view conversation", async () => {
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
);
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/9999"]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
},
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call endSession if the user has permission", async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue({
|
||||
|
||||
@@ -8,6 +8,7 @@ import MainApp from "#/routes/_oh/route";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/_oh._index/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
@@ -25,32 +26,32 @@ const createAxiosNotFoundErrorObject = () =>
|
||||
},
|
||||
);
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
// layout route
|
||||
Component: MainApp,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
// home route
|
||||
Component: Home,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Home Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
// layout route
|
||||
Component: MainApp,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
// home route
|
||||
Component: Home,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the home screen", () => {
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
});
|
||||
@@ -77,41 +78,82 @@ describe("Home Screen", () => {
|
||||
const settingsScreen = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreen).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings 404", () => {
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
describe("Settings 404", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
const settingsScreen = screen.queryByTestId("settings-screen");
|
||||
expect(settingsScreen).not.toBeInTheDocument();
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
const settingsScreen = screen.queryByTestId("settings-screen");
|
||||
expect(settingsScreen).not.toBeInTheDocument();
|
||||
|
||||
const advancedSettingsButton = await screen.findByTestId(
|
||||
"advanced-settings-link",
|
||||
);
|
||||
await user.click(advancedSettingsButton);
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
|
||||
const settingsScreenAfter = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreenAfter).toBeInTheDocument();
|
||||
const advancedSettingsButton = await screen.findByTestId(
|
||||
"advanced-settings-link",
|
||||
);
|
||||
await user.click(advancedSettingsButton);
|
||||
|
||||
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
|
||||
expect(settingsModalAfter).not.toBeInTheDocument();
|
||||
});
|
||||
const settingsScreenAfter = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreenAfter).toBeInTheDocument();
|
||||
|
||||
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
|
||||
expect(settingsModalAfter).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
|
||||
// TODO: Remove HIDE_LLM_SETTINGS check once released
|
||||
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({ APP_MODE: "saas" });
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Setup Payment modal", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should only render if SaaS mode and is new user", async () => {
|
||||
// @ts-expect-error - we only need the APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const setupPaymentModal = await screen.findByTestId("proceed-to-stripe-button");
|
||||
expect(setupPaymentModal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, describe, expect, it, test, vi } from "vitest";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent, { UserEvent } from "@testing-library/user-event";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
@@ -11,6 +20,7 @@ import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { PostApiSettings } from "#/types/settings";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
@@ -29,6 +39,11 @@ describe("Settings Screen", () => {
|
||||
useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Remove this once we release
|
||||
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -67,6 +82,14 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
describe("Account Settings", () => {
|
||||
beforeEach(() => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the account settings", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -280,6 +303,14 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
describe("LLM Settings", () => {
|
||||
beforeEach(() => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the basic LLM settings by default", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -427,8 +458,7 @@ describe("Settings Screen", () => {
|
||||
expect(input).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the runtime settings input if SaaS mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
it.skip("should render the runtime settings input if SaaS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
@@ -436,12 +466,10 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
screen.getByTestId("runtime-settings-input");
|
||||
await screen.findByTestId("runtime-settings-input");
|
||||
});
|
||||
|
||||
it("should set the default runtime setting set", async () => {
|
||||
it.skip("should set the default runtime setting set", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
@@ -455,13 +483,11 @@ describe("Settings Screen", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(userEvent.setup());
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
expect(input).toHaveValue("1x (2 core, 8G)");
|
||||
});
|
||||
|
||||
it("should always have the runtime input disabled", async () => {
|
||||
it.skip("should always have the runtime input disabled", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
@@ -470,8 +496,6 @@ describe("Settings Screen", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(userEvent.setup());
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
@@ -490,8 +514,6 @@ describe("Settings Screen", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
await user.click(input);
|
||||
|
||||
@@ -699,7 +721,7 @@ describe("Settings Screen", () => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
github_token: undefined,
|
||||
provider_tokens: undefined,
|
||||
language: "no",
|
||||
}),
|
||||
);
|
||||
@@ -736,7 +758,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
github_token: undefined,
|
||||
provider_tokens: undefined,
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
llm_model: "openai/gpt-4o",
|
||||
}),
|
||||
@@ -779,7 +801,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
...mockCopy,
|
||||
github_token: undefined, // not set
|
||||
provider_tokens: undefined, // not set
|
||||
llm_api_key: "", // reset as well
|
||||
});
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
@@ -953,4 +975,104 @@ describe("Settings Screen", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SaaS mode", () => {
|
||||
beforeEach(() => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
});
|
||||
|
||||
it("should hide the entire LLM section", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
|
||||
|
||||
const llmSection = screen.queryByTestId("llm-settings-section");
|
||||
expect(llmSection).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render LLM Provider, LLM Model, and LLM API Key inputs", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
|
||||
|
||||
const providerInput = screen.queryByTestId("llm-provider-input");
|
||||
const modelInput = screen.queryByTestId("llm-model-input");
|
||||
const apiKeyInput = screen.queryByTestId("llm-api-key-input");
|
||||
const llmApiKeyHelpAnchor = screen.queryByTestId(
|
||||
"llm-api-key-help-anchor",
|
||||
);
|
||||
const advancedSettingsSwitch = screen.queryByTestId(
|
||||
"advanced-settings-switch",
|
||||
);
|
||||
|
||||
expect(providerInput).not.toBeInTheDocument();
|
||||
expect(modelInput).not.toBeInTheDocument();
|
||||
expect(apiKeyInput).not.toBeInTheDocument();
|
||||
expect(llmApiKeyHelpAnchor).not.toBeInTheDocument();
|
||||
expect(advancedSettingsSwitch).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// We might want to bring this back if users wish to set advanced settings
|
||||
it.skip("should render advanced settings by default", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
|
||||
|
||||
const agentInput = screen.getByTestId("agent-input"); // this is only rendered in advanced mode
|
||||
expect(agentInput).toBeInTheDocument();
|
||||
|
||||
// we should not allow the user to change these fields
|
||||
const customModelInput = screen.queryByTestId("llm-custom-model-input");
|
||||
const baseUrlInput = screen.queryByTestId("base-url-input");
|
||||
|
||||
expect(customModelInput).not.toBeInTheDocument();
|
||||
expect(baseUrlInput).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not submit the unwanted fields when saving", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = await screen.findByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: undefined,
|
||||
llm_base_url: undefined,
|
||||
llm_model: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not submit the unwanted fields when resetting", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("Reset to defaults");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: undefined,
|
||||
llm_base_url: undefined,
|
||||
llm_model: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import { handleStatusMessage, handleActionMessage } from "#/services/actions";
|
||||
import store from "#/store";
|
||||
import { trackError } from "#/utils/error-handler";
|
||||
import ActionType from "#/types/action-type";
|
||||
import { ActionMessage } from "#/types/message";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("#/utils/error-handler", () => ({
|
||||
@@ -56,4 +58,89 @@ describe("Actions Service", () => {
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleActionMessage", () => {
|
||||
it("should use first-person perspective for task completion messages", () => {
|
||||
// Test partial completion
|
||||
const messagePartial: ActionMessage = {
|
||||
id: 1,
|
||||
action: ActionType.FINISH,
|
||||
source: "agent",
|
||||
message: "",
|
||||
timestamp: new Date().toISOString(),
|
||||
args: {
|
||||
final_thought: "",
|
||||
task_completed: "partial",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedPartialMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed partially**")) {
|
||||
capturedPartialMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messagePartial);
|
||||
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
|
||||
|
||||
// Test not completed
|
||||
const messageNotCompleted: ActionMessage = {
|
||||
id: 2,
|
||||
action: ActionType.FINISH,
|
||||
source: "agent",
|
||||
message: "",
|
||||
timestamp: new Date().toISOString(),
|
||||
args: {
|
||||
final_thought: "",
|
||||
task_completed: "false",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedNotCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **not completed**")) {
|
||||
capturedNotCompletedMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messageNotCompleted);
|
||||
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
|
||||
|
||||
// Test completed successfully
|
||||
const messageCompleted: ActionMessage = {
|
||||
id: 3,
|
||||
action: ActionType.FINISH,
|
||||
source: "agent",
|
||||
message: "",
|
||||
timestamp: new Date().toISOString(),
|
||||
args: {
|
||||
final_thought: "",
|
||||
task_completed: "true",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed successfully**")) {
|
||||
capturedCompletedMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messageCompleted);
|
||||
expect(capturedCompletedMessage).toContain("I believe that the task was **completed successfully**");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { trackError, showErrorToast, showChatError } from "#/utils/error-handler";
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
trackError,
|
||||
showErrorToast,
|
||||
showChatError,
|
||||
} from "#/utils/error-handler";
|
||||
import * as Actions from "#/services/actions";
|
||||
import * as CustomToast from "#/utils/custom-toast-handlers";
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
@@ -10,12 +14,6 @@ vi.mock("posthog-js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/services/actions", () => ({
|
||||
handleStatusMessage: vi.fn(),
|
||||
}));
|
||||
@@ -38,9 +36,12 @@ describe("Error Handler", () => {
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Test error"),
|
||||
{
|
||||
error_source: "test",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should include additional metadata in PostHog event", () => {
|
||||
@@ -55,15 +56,19 @@ describe("Error Handler", () => {
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Test error"),
|
||||
{
|
||||
error_source: "test",
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showErrorToast", () => {
|
||||
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
|
||||
it("should log error and show toast", () => {
|
||||
const error = {
|
||||
message: "Toast error",
|
||||
@@ -73,12 +78,15 @@ describe("Error Handler", () => {
|
||||
showErrorToast(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Toast error"),
|
||||
{
|
||||
error_source: "toast-test",
|
||||
},
|
||||
);
|
||||
|
||||
// Verify toast was shown
|
||||
expect(toast.error).toHaveBeenCalled();
|
||||
expect(errorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include metadata in PostHog event when showing toast", () => {
|
||||
@@ -90,10 +98,13 @@ describe("Error Handler", () => {
|
||||
|
||||
showErrorToast(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
context: "testing",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Toast error"),
|
||||
{
|
||||
error_source: "toast-test",
|
||||
context: "testing",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should log errors from different sources with appropriate metadata", () => {
|
||||
@@ -104,10 +115,13 @@ describe("Error Handler", () => {
|
||||
metadata: { id: "error.agent" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Agent error"), {
|
||||
error_source: "agent-status",
|
||||
id: "error.agent",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Agent error"),
|
||||
{
|
||||
error_source: "agent-status",
|
||||
id: "error.agent",
|
||||
},
|
||||
);
|
||||
|
||||
showErrorToast({
|
||||
message: "Server error",
|
||||
@@ -115,11 +129,14 @@ describe("Error Handler", () => {
|
||||
metadata: { error_code: 500, details: "Internal error" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Server error"), {
|
||||
error_source: "server",
|
||||
error_code: 500,
|
||||
details: "Internal error",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Server error"),
|
||||
{
|
||||
error_source: "server",
|
||||
error_code: 500,
|
||||
details: "Internal error",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should log feedback submission errors with conversation context", () => {
|
||||
@@ -130,11 +147,14 @@ describe("Error Handler", () => {
|
||||
metadata: { conversationId: "123", error },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Feedback submission failed"), {
|
||||
error_source: "feedback",
|
||||
conversationId: "123",
|
||||
error,
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Feedback submission failed"),
|
||||
{
|
||||
error_source: "feedback",
|
||||
conversationId: "123",
|
||||
error,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,9 +169,12 @@ describe("Error Handler", () => {
|
||||
showChatError(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Chat error"), {
|
||||
error_source: "chat-test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Chat error"),
|
||||
{
|
||||
error_source: "chat-test",
|
||||
},
|
||||
);
|
||||
|
||||
// Verify error message was shown in chat
|
||||
expect(Actions.handleStatusMessage).toHaveBeenCalledWith({
|
||||
|
||||
1385
frontend/package-lock.json
generated
1385
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.28.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -9,43 +9,43 @@
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.4",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.2.0",
|
||||
"@react-router/serve": "^7.2.0",
|
||||
"@react-types/shared": "^3.27.0",
|
||||
"@react-router/node": "^7.3.0",
|
||||
"@react-router/serve": "^7.3.0",
|
||||
"@react-types/shared": "^3.28.0",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.7.0",
|
||||
"@tanstack/react-query": "^5.66.11",
|
||||
"@stripe/react-stripe-js": "^3.3.0",
|
||||
"@stripe/stripe-js": "^5.10.0",
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.8.1",
|
||||
"axios": "^1.8.2",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.4.7",
|
||||
"framer-motion": "^12.4.10",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.23",
|
||||
"jose": "^6.0.8",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.225.1",
|
||||
"posthog-js": "^1.229.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.0.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.2.0",
|
||||
"react-router": "^7.3.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite": "^6.2.1",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
@@ -80,29 +80,29 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@react-router/dev": "^7.2.0",
|
||||
"@playwright/test": "^1.51.0",
|
||||
"@react-router/dev": "^7.3.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.66.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.67.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.13.8",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.0.7",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.7.0'
|
||||
const PACKAGE_VERSION = '2.7.3'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -281,6 +281,13 @@ class OpenHands {
|
||||
return data.redirect_url;
|
||||
}
|
||||
|
||||
static async createBillingSessionResponse(): Promise<string> {
|
||||
const { data } = await openHands.post(
|
||||
"/api/billing/create-customer-setup-session",
|
||||
);
|
||||
return data.redirect_url;
|
||||
}
|
||||
|
||||
static async getBalance(): Promise<string> {
|
||||
const { data } = await openHands.get<{ credits: string }>(
|
||||
"/api/billing/credits",
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface GetConfigResponse {
|
||||
APP_SLUG?: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
STRIPE_PUBLISHABLE_KEY?: string;
|
||||
}
|
||||
|
||||
export interface GetVSCodeUrlResponse {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@@ -23,6 +22,7 @@ import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bott
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-files";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@@ -98,7 +98,7 @@ export function ChatInterface() {
|
||||
|
||||
const onClickExportTrajectoryButton = () => {
|
||||
if (!params.conversationId) {
|
||||
toast.error("ConversationId unknown, cannot download trajectory");
|
||||
displayErrorToast("ConversationId unknown, cannot download trajectory");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function ChatInterface() {
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
export function SetupPaymentModal() {
|
||||
const { t } = useTranslation();
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: OpenHands.createBillingSessionResponse,
|
||||
onSuccess: (data) => {
|
||||
window.location.href = data;
|
||||
},
|
||||
onError: () => {
|
||||
displayErrorToast(t("BILLING$ERROR_WHILE_CREATING_SESSION"));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="border border-tertiary">
|
||||
<AllHandsLogo width={68} height={46} />
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">{t("BILLING$YOUVE_GOT_50")}</h1>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="BILLING$CLAIM_YOUR_50"
|
||||
components={{ b: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<BrandButton
|
||||
testId="proceed-to-stripe-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
isDisabled={isPending}
|
||||
onClick={mutate}
|
||||
>
|
||||
{t("BILLING$PROCEED_TO_STRIPE")}
|
||||
</BrandButton>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
@@ -10,7 +9,6 @@ import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-b
|
||||
import { DocsButton } from "#/components/shared/buttons/docs-button";
|
||||
import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button";
|
||||
import { SettingsButton } from "#/components/shared/buttons/settings-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
import { useCurrentSettings } from "#/context/settings-context";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
@@ -23,6 +21,8 @@ import { ConversationPanelWrapper } from "../conversation-panel/conversation-pan
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
@@ -43,7 +43,12 @@ export function Sidebar() {
|
||||
const [conversationPanelIsOpen, setConversationPanelIsOpen] =
|
||||
React.useState(false);
|
||||
|
||||
// TODO: Remove HIDE_LLM_SETTINGS check once released
|
||||
const isSaas = HIDE_LLM_SETTINGS() && config?.APP_MODE === "saas";
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isSaas) return;
|
||||
|
||||
if (location.pathname === "/settings") {
|
||||
setSettingsModalIsOpen(false);
|
||||
} else if (
|
||||
@@ -53,10 +58,10 @@ export function Sidebar() {
|
||||
) {
|
||||
// We don't show toast errors for settings in the global error handler
|
||||
// because we have a special case for 404 errors
|
||||
toast.error(
|
||||
displayErrorToast(
|
||||
"Something went wrong while fetching settings. Please reload the page.",
|
||||
);
|
||||
} else if (settingsError?.status === 404) {
|
||||
} else if (config?.APP_MODE === "oss" && settingsError?.status === 404) {
|
||||
setSettingsModalIsOpen(true);
|
||||
}
|
||||
}, [
|
||||
@@ -111,15 +116,13 @@ export function Sidebar() {
|
||||
>
|
||||
<SettingsButton />
|
||||
</NavLink>
|
||||
{!user.isLoading && (
|
||||
<UserActions
|
||||
user={
|
||||
user.data ? { avatar_url: user.data.avatar_url } : undefined
|
||||
}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
)}
|
||||
{user.isLoading && <LoadingSpinner size="small" />}
|
||||
<UserActions
|
||||
user={
|
||||
user.data ? { avatar_url: user.data.avatar_url } : undefined
|
||||
}
|
||||
onLogout={handleLogout}
|
||||
isLoading={user.isFetching}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import { AccountSettingsContextMenu } from "../context-menu/account-settings-con
|
||||
interface UserActionsProps {
|
||||
onLogout: () => void;
|
||||
user?: { avatar_url: string };
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function UserActions({ onLogout, user }: UserActionsProps) {
|
||||
export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] =
|
||||
React.useState(false);
|
||||
|
||||
@@ -26,7 +27,11 @@ export function UserActions({ onLogout, user }: UserActionsProps) {
|
||||
|
||||
return (
|
||||
<div data-testid="user-actions" className="w-8 h-8 relative">
|
||||
<UserAvatar avatarUrl={user?.avatar_url} onClick={toggleAccountMenu} />
|
||||
<UserAvatar
|
||||
avatarUrl={user?.avatar_url}
|
||||
onClick={toggleAccountMenu}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{accountContextMenuIsVisible && (
|
||||
<AccountSettingsContextMenu
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { MutateOptions } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { PostSettings, Settings } from "#/types/settings";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
type SaveUserSettingsConfig = {
|
||||
onSuccess: MutateOptions<void, Error, Partial<PostSettings>>["onSuccess"];
|
||||
@@ -47,7 +47,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
||||
onSuccess: config?.onSuccess,
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
toast.error(errorMessage);
|
||||
displayErrorToast(errorMessage);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
? ""
|
||||
: settings.LLM_API_KEY?.trim() || undefined,
|
||||
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
github_token: settings.github_token,
|
||||
provider_tokens: settings.provider_tokens,
|
||||
unset_github_token: settings.unset_github_token,
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { Feedback } from "#/api/open-hands.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
type SubmitFeedbackArgs = {
|
||||
feedback: Feedback;
|
||||
@@ -14,7 +14,7 @@ export const useSubmitFeedback = () => {
|
||||
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
|
||||
OpenHands.submitFeedback(conversationId, feedback),
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,4 +11,6 @@ export const useAIConfigOptions = () =>
|
||||
useQuery({
|
||||
queryKey: ["ai-config-options"],
|
||||
queryFn: fetchAiConfigOptions,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -14,5 +14,7 @@ export const useAppInstallations = () => {
|
||||
githubTokenIsSet &&
|
||||
!!config?.GITHUB_CLIENT_ID &&
|
||||
config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,6 +50,8 @@ export const useAppRepositories = () => {
|
||||
Array.isArray(installations) &&
|
||||
installations.length > 0 &&
|
||||
config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { BILLING_SETTINGS } from "#/utils/feature-flags";
|
||||
|
||||
export const useBalance = () => {
|
||||
const { data: config } = useConfig();
|
||||
@@ -8,6 +9,6 @@ export const useBalance = () => {
|
||||
return useQuery({
|
||||
queryKey: ["user", "balance"],
|
||||
queryFn: OpenHands.getBalance,
|
||||
enabled: config?.APP_MODE === "saas",
|
||||
enabled: config?.APP_MODE === "saas" && BILLING_SETTINGS(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,4 +5,6 @@ export const useConfig = () =>
|
||||
useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: OpenHands.getConfig,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ export const useConversationConfig = () => {
|
||||
return OpenHands.getRuntimeId(conversationId);
|
||||
},
|
||||
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -19,6 +19,8 @@ export const useGitHubUser = () => {
|
||||
queryFn: OpenHands.getGitHubUser,
|
||||
enabled: githubTokenIsSet && !!config?.APP_MODE,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const useIsAuthed = () => {
|
||||
queryFn: () => OpenHands.authenticate(appMode!),
|
||||
enabled: !!appMode,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
retry: false,
|
||||
meta: {
|
||||
disableToast: true,
|
||||
|
||||
@@ -23,5 +23,7 @@ export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
|
||||
queryKey: ["files", conversationId, config?.path],
|
||||
queryFn: () => OpenHands.getFiles(conversationId, config?.path),
|
||||
enabled: !!(isActive && config?.enabled),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,5 +7,7 @@ export function useSearchRepositories(query: string) {
|
||||
queryFn: () => OpenHands.searchGitHubRepositories(query, 3),
|
||||
enabled: !!query,
|
||||
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const getSettingsQueryFn = async () => {
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
|
||||
PROVIDER_TOKENS: apiSettings.provider_tokens,
|
||||
IS_NEW_USER: false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,6 +36,8 @@ export const useSettings = () => {
|
||||
// would want to show the modal immediately if the
|
||||
// settings are not found
|
||||
retry: (_, error) => error.status !== 404,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
|
||||
@@ -7,4 +7,6 @@ export const useUserConversation = (cid: string | null) =>
|
||||
queryFn: () => OpenHands.getConversation(cid!),
|
||||
enabled: !!cid,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -9,6 +9,5 @@ export const useUserConversations = () => {
|
||||
queryKey: ["user", "conversations"],
|
||||
queryFn: OpenHands.getUserConversations,
|
||||
enabled: !!userIsAuthenticated,
|
||||
staleTime: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@ export const useUserRepositories = () => {
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
enabled: githubTokenIsSet && config?.APP_MODE === "oss",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
|
||||
@@ -13,6 +13,8 @@ export const useVSCodeUrl = (config: { enabled: boolean }) => {
|
||||
},
|
||||
enabled: !!conversationId && config.enabled,
|
||||
refetchOnMount: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
@@ -86,6 +86,8 @@ export const useTerminal = ({
|
||||
|
||||
const handleEnter = (command: string) => {
|
||||
terminal.current?.write("\r\n");
|
||||
// Send the command to the backend but don't echo it back in the terminal
|
||||
// The backend will include the command in its response
|
||||
send(getTerminalCommand(command));
|
||||
};
|
||||
|
||||
@@ -131,10 +133,26 @@ export const useTerminal = ({
|
||||
content = content.replaceAll(secret, "*".repeat(10));
|
||||
});
|
||||
|
||||
terminal.current?.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
);
|
||||
// Check if this is an output that starts with the previous input command
|
||||
// This happens when the backend echoes back the command in the output
|
||||
let shouldDisplayContent = true;
|
||||
if (type === "output" && i > 0 && commands[i - 1].type === "input") {
|
||||
const prevInputCommand = commands[i - 1].content.trim();
|
||||
// If the output starts with the input command, remove it to avoid duplication
|
||||
if (content.trim().startsWith(prevInputCommand)) {
|
||||
// Skip displaying this part as it's a duplicate of the user's input
|
||||
// that's already shown in the terminal
|
||||
shouldDisplayContent = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisplayContent) {
|
||||
terminal.current?.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
);
|
||||
}
|
||||
|
||||
// Add a new prompt after the output
|
||||
if (type === "output") {
|
||||
terminal.current.write(`\n$ `);
|
||||
}
|
||||
|
||||
@@ -312,4 +312,9 @@ export enum I18nKey {
|
||||
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
|
||||
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
|
||||
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
|
||||
BILLING$YOUVE_GOT_50 = "BILLING$YOUVE_GOT_50",
|
||||
BILLING$ERROR_WHILE_CREATING_SESSION = "BILLING$ERROR_WHILE_CREATING_SESSION",
|
||||
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
|
||||
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
|
||||
BILLING$YOURE_IN = "BILLING$YOURE_IN",
|
||||
}
|
||||
|
||||
@@ -4647,5 +4647,80 @@
|
||||
"fr": "Ajouter des fonds à votre compte",
|
||||
"tr": "Hesabınıza bakiye ekleyin",
|
||||
"de": "Guthaben zu Ihrem Konto hinzufügen"
|
||||
},
|
||||
"BILLING$YOUVE_GOT_50": {
|
||||
"en": "You've got $50 in free OpenHands credits",
|
||||
"ja": "OpenHandsの無料クレジット$50を獲得しました",
|
||||
"zh-CN": "您获得了 $50 的 OpenHands 免费额度",
|
||||
"zh-TW": "您獲得了 $50 的 OpenHands 免費額度",
|
||||
"ko-KR": "OpenHands 무료 크레딧 $50를 받았습니다",
|
||||
"no": "Du har fått $50 i gratis OpenHands-kreditter",
|
||||
"it": "Hai ottenuto $50 in crediti gratuiti OpenHands",
|
||||
"pt": "Você ganhou $50 em créditos gratuitos OpenHands",
|
||||
"es": "Has recibido $50 en créditos gratuitos de OpenHands",
|
||||
"ar": "لديك 50$ من رصيد OpenHands المجاني",
|
||||
"fr": "Vous avez reçu $50 de crédits OpenHands gratuits",
|
||||
"tr": "OpenHands'de $50 ücretsiz kredi kazandınız",
|
||||
"de": "Sie haben $50 in kostenlosen OpenHands-Guthaben erhalten"
|
||||
},
|
||||
"BILLING$ERROR_WHILE_CREATING_SESSION": {
|
||||
"en": "Error occurred while setting up your payment session. Please try again later.",
|
||||
"ja": "お支払いセッションの設定中にエラーが発生しました。後ほど再度お試しください。",
|
||||
"zh-CN": "设置支付会话时发生错误。请稍后再试。",
|
||||
"zh-TW": "設置支付會話時發生錯誤。請稍後再試。",
|
||||
"ko-KR": "결제 세션 설정 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"no": "Det oppstod en feil under oppsett av betalingsøkten. Vennligst prøv igjen senere.",
|
||||
"it": "Si è verificato un errore durante la configurazione della sessione di pagamento. Si prega di riprovare più tardi.",
|
||||
"pt": "Ocorreu um erro ao configurar sua sessão de pagamento. Por favor, tente novamente mais tarde.",
|
||||
"es": "Se produjo un error al configurar tu sesión de pago. Por favor, inténtalo de nuevo más tarde.",
|
||||
"ar": "حدث خطأ أثناء إعداد جلسة الدفع الخاصة بك. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"fr": "Une erreur s'est produite lors de la configuration de votre session de paiement. Veuillez réessayer plus tard.",
|
||||
"tr": "Ödeme oturumunuz kurulurken bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
|
||||
"de": "Beim Einrichten Ihrer Zahlungssitzung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut."
|
||||
},
|
||||
"BILLING$CLAIM_YOUR_50": {
|
||||
"en": "Add a credit card with Stripe to claim your $50. <b>We won't charge you without asking first!</b>",
|
||||
"ja": "Stripeでクレジットカードを追加して$50を獲得。<b>事前の確認なしで請求することはありません!</b>",
|
||||
"zh-CN": "添加 Stripe 信用卡以领取 $50。<b>我们不会在未经您同意的情况下收费!</b>",
|
||||
"zh-TW": "添加 Stripe 信用卡以領取 $50。<b>我們不會在未經您同意的情況下收費!</b>",
|
||||
"ko-KR": "Stripe에 신용카드를 추가하여 $50를 받으세요. <b>사전 동의 없이 요금이 청구되지 않습니다!</b>",
|
||||
"no": "Legg til et kredittkort med Stripe for å få $50. <b>Vi belaster deg ikke uten å spørre først!</b>",
|
||||
"it": "Aggiungi una carta di credito con Stripe per ottenere $50. <b>Non ti addebiteremo nulla senza chiedere prima!</b>",
|
||||
"pt": "Adicione um cartão de crédito com Stripe para receber $50. <b>Não cobraremos sem perguntar primeiro!</b>",
|
||||
"es": "Añade una tarjeta de crédito con Stripe para reclamar tus $50. <b>¡No te cobraremos sin preguntarte primero!</b>",
|
||||
"ar": "أضف بطاقة ائتمان مع Stripe للحصول على 50$. <b>لن نقوم بالخصم دون إذن مسبق!</b>",
|
||||
"fr": "Ajoutez une carte de crédit avec Stripe pour obtenir 50$. <b>Nous ne vous facturerons pas sans vous demander d'abord !</b>",
|
||||
"tr": "50$ almak için Stripe ile kredi kartı ekleyin. <b>Önce sormadan ücret almayacağız!</b>",
|
||||
"de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. <b>Wir belasten Sie nicht ohne vorherige Zustimmung!</b>"
|
||||
},
|
||||
"BILLING$PROCEED_TO_STRIPE": {
|
||||
"en": "Add Billing Info",
|
||||
"ja": "請求情報を追加",
|
||||
"zh-CN": "添加账单信息",
|
||||
"zh-TW": "添加帳單資訊",
|
||||
"ko-KR": "결제 정보 추가",
|
||||
"no": "Legg til betalingsinformasjon",
|
||||
"it": "Aggiungi informazioni di fatturazione",
|
||||
"pt": "Adicionar informações de pagamento",
|
||||
"es": "Añadir información de facturación",
|
||||
"ar": "إضافة معلومات الفواتير",
|
||||
"fr": "Ajouter les informations de facturation",
|
||||
"tr": "Fatura Bilgisi Ekle",
|
||||
"de": "Zahlungsinformationen hinzufügen"
|
||||
},
|
||||
"BILLING$YOURE_IN": {
|
||||
"en": "You're in! You can start using your $50 in free credits now.",
|
||||
"ja": "登録完了!$50分の無料クレジットを今すぐご利用いただけます。",
|
||||
"zh-CN": "您已加入!现在可以开始使用$50的免费额度了。",
|
||||
"zh-TW": "您已加入!現在可以開始使用$50的免費額度了。",
|
||||
"ko-KR": "가입 완료! 지금 바로 $50 상당의 무료 크레딧을 사용하실 수 있습니다.",
|
||||
"no": "Du er med! Du kan begynne å bruke dine $50 i gratis kreditter nå.",
|
||||
"it": "Ci sei! Puoi iniziare a utilizzare i tuoi $50 in crediti gratuiti ora.",
|
||||
"pt": "Você está dentro! Você pode começar a usar seus $50 em créditos gratuitos agora.",
|
||||
"es": "¡Ya estás dentro! Puedes empezar a usar tus $50 en créditos gratuitos ahora.",
|
||||
"ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 50 دولارًا الآن.",
|
||||
"fr": "C'est fait ! Vous pouvez commencer à utiliser vos 50 $ de crédits gratuits maintenant.",
|
||||
"tr": "Başardın! Şimdi $50 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.",
|
||||
"de": "Du bist dabei! Du kannst jetzt deine $50 an kostenlosen Guthaben nutzen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.69949 5.72974L7.91965 7.9505L8.35077 7.51999L6.13001 5.29922L8.35077 3.07907L7.92026 2.64795L5.69949 4.86871L3.47934 2.64795L3.04883 3.07907L5.26898 5.29922L3.04883 7.51938L3.47934 7.9505L5.69949 5.72974Z"
|
||||
fill="black" />
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 13.0602L16.2123 16.7725L17.273 15.7118L13.5607 11.9995L17.273 8.28723L16.2123 7.22657L12.5 10.9389L8.78771 7.22656L7.72705 8.28722L11.4394 11.9995L7.72706 15.7119L8.78772 16.7725L12.5 13.0602Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 327 B |
@@ -1,8 +1,4 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const TEST_STRIPE_SECRET_KEY = "";
|
||||
const PRICE_ID = "";
|
||||
|
||||
export const STRIPE_BILLING_HANDLERS = [
|
||||
http.get("/api/billing/credits", async () => {
|
||||
@@ -10,27 +6,17 @@ export const STRIPE_BILLING_HANDLERS = [
|
||||
return HttpResponse.json({ credits: "100" });
|
||||
}),
|
||||
|
||||
http.post("/api/billing/create-checkout-session", async ({ request }) => {
|
||||
http.post("/api/billing/create-checkout-session", async () => {
|
||||
await delay();
|
||||
const body = await request.json();
|
||||
return HttpResponse.json({
|
||||
redirect_url: "https://stripe.com/some-checkout",
|
||||
});
|
||||
}),
|
||||
|
||||
if (body && typeof body === "object" && body.amount) {
|
||||
const stripe = new Stripe(TEST_STRIPE_SECRET_KEY);
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: PRICE_ID,
|
||||
quantity: body.amount,
|
||||
},
|
||||
],
|
||||
mode: "payment",
|
||||
success_url: "http://localhost:3001/settings/billing/?checkout=success",
|
||||
cancel_url: "http://localhost:3001/settings/billing/?checkout=cancel",
|
||||
});
|
||||
|
||||
if (session.url) return HttpResponse.json({ redirect_url: session.url });
|
||||
}
|
||||
|
||||
return HttpResponse.json({ message: "Invalid request" }, { status: 400 });
|
||||
http.post("/api/billing/create-customer-setup-session", async () => {
|
||||
await delay();
|
||||
return HttpResponse.json({
|
||||
redirect_url: "https://stripe.com/some-customer-setup",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -22,12 +22,13 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
|
||||
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
|
||||
};
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: ApiSettings | PostApiSettings;
|
||||
settings: ApiSettings | PostApiSettings | null;
|
||||
} = {
|
||||
settings: MOCK_DEFAULT_USER_SETTINGS,
|
||||
settings: null,
|
||||
};
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
@@ -174,22 +175,24 @@ export const handlers = [
|
||||
),
|
||||
http.get("/api/options/config", () => {
|
||||
const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true";
|
||||
|
||||
const config: GetConfigResponse = {
|
||||
APP_MODE: mockSaas ? "saas" : "oss",
|
||||
GITHUB_CLIENT_ID: "fake-github-client-id",
|
||||
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
|
||||
STRIPE_PUBLISHABLE_KEY: "",
|
||||
};
|
||||
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
const settings: ApiSettings = {
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
language: "no",
|
||||
};
|
||||
// @ts-expect-error - mock types
|
||||
if (settings.github_token) settings.github_token_is_set = true;
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens).length > 0)
|
||||
settings.github_token_is_set = true;
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
@@ -201,17 +204,19 @@ export const handlers = [
|
||||
if (typeof body === "object") {
|
||||
newSettings = { ...body };
|
||||
if (newSettings.unset_github_token) {
|
||||
newSettings.github_token = undefined;
|
||||
newSettings.provider_tokens = { github: "", gitlab: "" };
|
||||
newSettings.github_token_is_set = false;
|
||||
delete newSettings.unset_github_token;
|
||||
}
|
||||
}
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = {
|
||||
const fullSettings = {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
...newSettings,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = fullSettings;
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
QueryCache,
|
||||
MutationCache,
|
||||
} from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
|
||||
const shownErrors = new Set<string>();
|
||||
export const queryClientConfig: QueryClientConfig = {
|
||||
@@ -14,7 +14,7 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
|
||||
if (!shownErrors.has(errorMessage)) {
|
||||
toast.error(errorMessage || "An error occurred");
|
||||
displayErrorToast(errorMessage || "An error occurred");
|
||||
shownErrors.add(errorMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -28,14 +28,8 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
onError: (error, _, __, mutation) => {
|
||||
if (!mutation?.meta?.disableToast) {
|
||||
const message = retrieveAxiosErrorMessage(error);
|
||||
toast.error(message);
|
||||
displayErrorToast(message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,10 @@ function Home() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2">
|
||||
<div
|
||||
data-testid="home-screen"
|
||||
className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2"
|
||||
>
|
||||
<HeroHeading />
|
||||
<div className="flex flex-col gap-8 w-full md:w-[600px] items-center">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setImportedProjectZip } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
@@ -7,6 +6,7 @@ import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { useUploadFiles } from "../../../hooks/mutation/use-upload-files";
|
||||
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
export const useHandleRuntimeActive = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,7 +29,7 @@ export const useHandleRuntimeActive = () => {
|
||||
{ files: [file] },
|
||||
{
|
||||
onError: () => {
|
||||
toast.error("Failed to upload project files.");
|
||||
displayErrorToast("Failed to upload project files.");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
@@ -7,6 +6,7 @@ import { addErrorMessage } from "#/state/chat-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { useEndSession } from "../../../hooks/use-end-session";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
@@ -32,15 +32,15 @@ export const useHandleWSEvents = () => {
|
||||
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
displayErrorToast("Session expired.");
|
||||
endSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.error === "string") {
|
||||
toast.error(event.error);
|
||||
displayErrorToast(event.error);
|
||||
} else {
|
||||
toast.error(event.message);
|
||||
displayErrorToast(event.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FaServer } from "react-icons/fa";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
@@ -36,6 +35,7 @@ import { TerminalStatusLabel } from "#/components/features/terminal/terminal-sta
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@@ -66,7 +66,7 @@ function AppContent() {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFetched && !conversation) {
|
||||
toast.error(
|
||||
displayErrorToast(
|
||||
"This conversation does not exist, or you do not have permission to access it.",
|
||||
);
|
||||
endSession();
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React from "react";
|
||||
import { useRouteError, isRouteErrorResponse, Outlet } from "react-router";
|
||||
import {
|
||||
useRouteError,
|
||||
isRouteErrorResponse,
|
||||
Outlet,
|
||||
useNavigate,
|
||||
useLocation,
|
||||
useSearchParams,
|
||||
} from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n from "#/i18n";
|
||||
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
@@ -10,6 +18,10 @@ import { AnalyticsConsentFormModal } from "#/components/features/analytics/analy
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
|
||||
import { useBalance } from "#/hooks/query/use-balance";
|
||||
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
||||
import { BILLING_SETTINGS } from "#/utils/feature-flags";
|
||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@@ -44,11 +56,14 @@ export function ErrorBoundary() {
|
||||
}
|
||||
|
||||
export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { githubTokenIsSet } = useAuth();
|
||||
const { data: settings } = useSettings();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
|
||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = useConfig();
|
||||
const {
|
||||
@@ -62,6 +77,8 @@ export default function MainApp() {
|
||||
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
|
||||
});
|
||||
|
||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings?.LANGUAGE) {
|
||||
i18n.changeLanguage(settings.LANGUAGE);
|
||||
@@ -84,6 +101,17 @@ export default function MainApp() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Don't allow users to use the app if it 402s
|
||||
if (error?.status === 402 && pathname !== "/") {
|
||||
navigate("/");
|
||||
} else if (!isFetching && searchParams.get("free_credits") === "success") {
|
||||
displaySuccessToast(t("BILLING$YOURE_IN"));
|
||||
searchParams.delete("free_credits");
|
||||
navigate("/");
|
||||
}
|
||||
}, [error?.status, pathname, isFetching]);
|
||||
|
||||
const userIsAuthed = !!isAuthed && !authError;
|
||||
const renderWaitlistModal =
|
||||
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
|
||||
@@ -116,6 +144,10 @@ export default function MainApp() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{BILLING_SETTINGS() &&
|
||||
config.data?.APP_MODE === "saas" &&
|
||||
settings?.IS_NEW_USER && <SetupPaymentModal />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { PostSettings } from "#/types/settings";
|
||||
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
|
||||
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: 1, label: "1x (2 core, 8G)" },
|
||||
@@ -50,18 +52,25 @@ function AccountSettings() {
|
||||
const isFetching = isFetchingSettings || isFetchingResources;
|
||||
const isSuccess = isSuccessfulSettings && isSuccessfulResources;
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const shouldHandleSpecialSaasCase = HIDE_LLM_SETTINGS() && isSaas;
|
||||
|
||||
const determineWhetherToToggleAdvancedSettings = () => {
|
||||
if (shouldHandleSpecialSaasCase) return true;
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
isCustomModel(resources.models, settings.LLM_MODEL) ||
|
||||
hasAdvancedSettingsSet(settings)
|
||||
hasAdvancedSettingsSet({
|
||||
...settings,
|
||||
PROVIDER_TOKENS: settings.PROVIDER_TOKENS || {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const hasAppSlug = !!config?.APP_SLUG;
|
||||
const isGitHubTokenSet = settings?.GITHUB_TOKEN_IS_SET;
|
||||
const isLLMKeySet = settings?.LLM_API_KEY === "**********";
|
||||
@@ -106,58 +115,81 @@ function AccountSettings() {
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
formData.get("llm-api-key-input")?.toString() ||
|
||||
(isLLMKeySet
|
||||
? undefined // don't update if it's already set
|
||||
: ""); // reset if it's first time save to avoid 500 error
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
github_token:
|
||||
formData.get("github-token-input")?.toString() || undefined,
|
||||
LANGUAGE: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: customLlmModel || fullLlmModel,
|
||||
LLM_BASE_URL: formData.get("base-url-input")?.toString() || "",
|
||||
LLM_API_KEY:
|
||||
formData.get("llm-api-key-input")?.toString() ||
|
||||
(isLLMKeySet
|
||||
? undefined // don't update if it's already set
|
||||
: ""), // reset if it's first time save to avoid 500 error
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor ||
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
const finalLlmModel = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: customLlmModel || fullLlmModel;
|
||||
const finalLlmBaseUrl = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const newSettings = {
|
||||
github_token: githubToken,
|
||||
provider_tokens: githubToken
|
||||
? {
|
||||
github: githubToken,
|
||||
gitlab: "",
|
||||
}
|
||||
: undefined,
|
||||
LANGUAGE: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: finalLlmModel,
|
||||
LLM_BASE_URL: finalLlmBaseUrl,
|
||||
LLM_API_KEY: finalLlmApiKey,
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor ||
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
saveSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userConsentsToAnalytics);
|
||||
displaySuccessToast("Settings saved");
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userConsentsToAnalytics);
|
||||
displaySuccessToast("Settings saved");
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage);
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
saveSettings(
|
||||
{
|
||||
...DEFAULT_SETTINGS,
|
||||
LLM_API_KEY: "", // reset LLM API key
|
||||
const newSettings: Partial<PostSettings> = {
|
||||
...DEFAULT_SETTINGS,
|
||||
LLM_API_KEY: "", // reset LLM API key
|
||||
};
|
||||
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
// and we should make sure they aren't included in the reset
|
||||
if (shouldHandleSpecialSaasCase) {
|
||||
delete newSettings.LLM_API_KEY;
|
||||
delete newSettings.LLM_BASE_URL;
|
||||
delete newSettings.LLM_MODEL;
|
||||
}
|
||||
|
||||
saveSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
displaySuccessToast("Settings reset");
|
||||
setResetSettingsModalIsOpen(false);
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
displaySuccessToast("Settings reset");
|
||||
setResetSettingsModalIsOpen(false);
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -190,150 +222,162 @@ function AccountSettings() {
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
data-testid="account-settings-form"
|
||||
ref={formRef}
|
||||
action={onSubmit}
|
||||
className="flex flex-col grow overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col gap-12 px-11 py-9">
|
||||
<section className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-7">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
LLM Settings
|
||||
</h2>
|
||||
<SettingsSwitch
|
||||
testId="advanced-settings-switch"
|
||||
defaultIsToggled={isAdvancedSettingsSet}
|
||||
onToggle={onToggleAdvancedMode}
|
||||
>
|
||||
Advanced
|
||||
</SettingsSwitch>
|
||||
</div>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<section
|
||||
data-testid="llm-settings-section"
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<div className="flex items-center gap-7">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
LLM Settings
|
||||
</h2>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsSwitch
|
||||
testId="advanced-settings-switch"
|
||||
defaultIsToggled={isAdvancedSettingsSet}
|
||||
onToggle={onToggleAdvancedMode}
|
||||
>
|
||||
Advanced
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{llmConfigMode === "basic" && (
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
)}
|
||||
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label="Custom Model"
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label="Base URL"
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label="Custom Model"
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label="Base URL"
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label="API Key"
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
|
||||
}
|
||||
placeholder={isLLMKeySet ? "**********" : ""}
|
||||
/>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label="API Key"
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
|
||||
}
|
||||
placeholder={isLLMKeySet ? "**********" : ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text="Don't know your API key?"
|
||||
linkText="Click here for instructions"
|
||||
href="https://docs.all-hands.dev/modules/usage/llms"
|
||||
/>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text="Don't know your API key?"
|
||||
linkText="Click here for instructions"
|
||||
href="https://docs.all-hands.dev/modules/usage/llms"
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label="Agent"
|
||||
items={
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSaas && llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="runtime-settings-input"
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
Runtime Settings (
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
get in touch for access
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
Enable confirmation mode
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
Enable memory condensation
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
|
||||
<div>
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label="Security Analyzer"
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label="Agent"
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{isSaas && llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="runtime-settings-input"
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
Runtime Settings (
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
get in touch for access
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
Enable confirmation mode
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
Enable memory condensation
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
|
||||
<div>
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label="Security Analyzer"
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
|
||||
@@ -62,13 +62,12 @@ const messageActions = {
|
||||
let successPrediction = "";
|
||||
if (message.args.task_completed === "partial") {
|
||||
successPrediction =
|
||||
"The agent thinks that the task was **completed partially**.";
|
||||
"I believe that the task was **completed partially**.";
|
||||
} else if (message.args.task_completed === "false") {
|
||||
successPrediction =
|
||||
"The agent thinks that the task was **not completed**.";
|
||||
successPrediction = "I believe that the task was **not completed**.";
|
||||
} else if (message.args.task_completed === "true") {
|
||||
successPrediction =
|
||||
"The agent thinks that the task was **completed successfully**.";
|
||||
"I believe that the task was **completed successfully**.";
|
||||
}
|
||||
if (successPrediction) {
|
||||
// if final_thought is not empty, add a new line before the success prediction
|
||||
|
||||
@@ -15,6 +15,11 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
PROVIDER_TOKENS: {
|
||||
github: "",
|
||||
gitlab: "",
|
||||
},
|
||||
IS_NEW_USER: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface InitConfig {
|
||||
LLM_MODEL: string;
|
||||
};
|
||||
token?: string;
|
||||
github_token?: string;
|
||||
latest_event_id?: unknown; // Not sure what this is
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type Provider = "github" | "gitlab";
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
LLM_BASE_URL: string;
|
||||
@@ -11,6 +13,8 @@ export type Settings = {
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
PROVIDER_TOKENS: Record<Provider, string>;
|
||||
IS_NEW_USER?: boolean;
|
||||
};
|
||||
|
||||
export type ApiSettings = {
|
||||
@@ -26,16 +30,17 @@ export type ApiSettings = {
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
github_token: string;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
unset_github_token: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
};
|
||||
|
||||
export type PostApiSettings = ApiSettings & {
|
||||
github_token: string;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
unset_github_token: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const displayErrorToast = (error: string) => {
|
||||
toast.error(error, {
|
||||
position: "top-right",
|
||||
style: {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const displaySuccessToast = (message: string) => {
|
||||
toast.success(message, {
|
||||
position: "top-right",
|
||||
style: {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
};
|
||||
22
frontend/src/utils/custom-toast-handlers.tsx
Normal file
22
frontend/src/utils/custom-toast-handlers.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CSSProperties } from "react";
|
||||
import toast, { ToastOptions } from "react-hot-toast";
|
||||
|
||||
const TOAST_STYLE: CSSProperties = {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
};
|
||||
|
||||
const TOAST_OPTIONS: ToastOptions = {
|
||||
position: "top-right",
|
||||
style: TOAST_STYLE,
|
||||
};
|
||||
|
||||
export const displayErrorToast = (error: string) => {
|
||||
toast.error(error, TOAST_OPTIONS);
|
||||
};
|
||||
|
||||
export const displaySuccessToast = (message: string) => {
|
||||
toast.success(message, TOAST_OPTIONS);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import { displayErrorToast } from "./custom-toast-handlers";
|
||||
|
||||
interface ErrorDetails {
|
||||
message: string;
|
||||
@@ -23,7 +23,7 @@ export function showErrorToast({
|
||||
metadata = {},
|
||||
}: ErrorDetails) {
|
||||
trackError({ message, source, metadata });
|
||||
toast.error(message);
|
||||
displayErrorToast(message);
|
||||
}
|
||||
|
||||
export function showChatError({
|
||||
|
||||
@@ -13,3 +13,4 @@ function loadFeatureFlag(
|
||||
}
|
||||
|
||||
export const BILLING_SETTINGS = () => loadFeatureFlag("BILLING_SETTINGS");
|
||||
export const HIDE_LLM_SETTINGS = () => loadFeatureFlag("HIDE_LLM_SETTINGS");
|
||||
|
||||
@@ -59,6 +59,18 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
|
||||
ENABLE_DEFAULT_CONDENSER,
|
||||
} = extractAdvancedFormData(formData);
|
||||
|
||||
// Extract provider tokens
|
||||
const githubToken = formData.get("github-token")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token")?.toString();
|
||||
const providerTokens: Record<string, string> = {};
|
||||
|
||||
if (githubToken) {
|
||||
providerTokens.github = githubToken;
|
||||
}
|
||||
if (gitlabToken) {
|
||||
providerTokens.gitlab = gitlabToken;
|
||||
}
|
||||
|
||||
return {
|
||||
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
LLM_API_KEY,
|
||||
@@ -68,5 +80,6 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
|
||||
CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER,
|
||||
PROVIDER_TOKENS: providerTokens,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
tertiary: "#454545", // gray, used for inputs
|
||||
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
|
||||
content: "#ECEDEE", // light gray, used mostly for text
|
||||
"content-2": "#F9FBFE",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ the GitHub API.
|
||||
You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
|
||||
ALWAYS use the GitHub API for operations instead of a web browser.
|
||||
|
||||
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
|
||||
83
microagents/knowledge/swift-linux.md
Normal file
83
microagents/knowledge/swift-linux.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: swift-linux
|
||||
type: knowledge
|
||||
agent: CodeActAgent
|
||||
version: 1.0.0
|
||||
triggers:
|
||||
- swift-linux
|
||||
- swift-debian
|
||||
- swift-installation
|
||||
---
|
||||
|
||||
# Swift Installation Guide for Debian Linux
|
||||
|
||||
This document provides instructions for installing Swift on Debian 12 (Bookworm).
|
||||
|
||||
> This setup is intended for non-UI development tasks on Swift on Linux.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing Swift, you need to install the required dependencies for your system. You can find the most up-to-date list of dependencies for your specific Linux distribution and version at the [Swift.org tarball installation guide](https://www.swift.org/install/linux/tarball/).
|
||||
|
||||
FOR EXAMPLE, the dependencies you may need to install for Debian 12 could be:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
binutils-gold \
|
||||
gcc \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libedit-dev \
|
||||
libicu-dev \
|
||||
libncurses-dev \
|
||||
libpython3-dev \
|
||||
libsqlite3-dev \
|
||||
libxml2-dev \
|
||||
pkg-config \
|
||||
tzdata \
|
||||
uuid-dev
|
||||
```
|
||||
|
||||
## Download and Install Swift
|
||||
|
||||
1. Find the latest Swift version for Debian:
|
||||
|
||||
Go to the [Swift.org download page](https://www.swift.org/download/) to find the latest Swift version compatible with Debian 12 (Bookworm).
|
||||
|
||||
Look for a tarball named something like `swift-<VERSION>-RELEASE-debian12.tar.gz` (e.g., `swift-6.0.3-RELEASE-debian12.tar.gz`).
|
||||
|
||||
The URL pattern is typically:
|
||||
```
|
||||
https://download.swift.org/swift-<VERSION>-release/debian12/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE-debian12.tar.gz
|
||||
```
|
||||
|
||||
Where `<VERSION>` is the Swift version number (e.g., `6.0.3`).
|
||||
|
||||
2. Download the Swift binary for Debian 12:
|
||||
|
||||
```bash
|
||||
cd /workspace
|
||||
wget https://download.swift.org/swift-6.0.3-release/debian12/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-debian12.tar.gz
|
||||
```
|
||||
|
||||
3. Extract the archive:
|
||||
|
||||
> **Note**: Make sure to install Swift in the `/workspace` directory, but outside the git repository to avoid committing the Swift binaries.
|
||||
|
||||
4. Add Swift to your PATH by adding the following line to your `~/.bashrc` file:
|
||||
|
||||
```bash
|
||||
echo 'export PATH=/workspace/swift-6.0.3-RELEASE-debian12/usr/bin:$PATH' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
> **Note**: Make sure to update the version number in the PATH to match the version you downloaded.
|
||||
|
||||
## Verify Installation
|
||||
|
||||
Verify that Swift is correctly installed by running:
|
||||
|
||||
```bash
|
||||
swift --version
|
||||
```
|
||||
@@ -1,9 +1,5 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from openhands.agenthub.micro.agent import MicroAgent
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@@ -14,26 +10,13 @@ from openhands.agenthub import ( # noqa: E402
|
||||
dummy_agent,
|
||||
visualbrowsing_agent,
|
||||
)
|
||||
from openhands.controller.agent import Agent # noqa: E402
|
||||
|
||||
__all__ = [
|
||||
'Agent',
|
||||
'codeact_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent',
|
||||
'browsing_agent',
|
||||
'visualbrowsing_agent',
|
||||
]
|
||||
|
||||
for agent in all_microagents.values():
|
||||
name = agent['name']
|
||||
prompt = agent['prompt']
|
||||
|
||||
anon_class = type(
|
||||
name,
|
||||
(MicroAgent,),
|
||||
{
|
||||
'prompt': prompt,
|
||||
'agent_definition': agent,
|
||||
},
|
||||
)
|
||||
|
||||
Agent.register(name, anon_class)
|
||||
|
||||
@@ -1,11 +1,63 @@
|
||||
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
|
||||
<IMPORTANT>
|
||||
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
|
||||
|
||||
<ROLE>
|
||||
Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed.
|
||||
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
|
||||
</ROLE>
|
||||
|
||||
<EFFICIENCY>
|
||||
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
|
||||
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
|
||||
</EFFICIENCY>
|
||||
|
||||
<FILE_SYSTEM_GUIDELINES>
|
||||
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
|
||||
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
|
||||
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
|
||||
</FILE_SYSTEM_GUIDELINES>
|
||||
|
||||
<CODE_QUALITY>
|
||||
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
|
||||
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
|
||||
* Before implementing any changes, first thoroughly understand the codebase through exploration.
|
||||
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
|
||||
</CODE_QUALITY>
|
||||
|
||||
<VERSION_CONTROL>
|
||||
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* You MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
|
||||
* If the user asks you to edit a file, you should edit the file directly, do NOT create a new file with the updated content unless the user explicitly instructs you to do so.
|
||||
* When you are doing global search-and-replace, consider using `sed` instead of running file editor multiple times.
|
||||
* Only use GITHUB_TOKEN and other credentials in ways that the user has asked for and would expect. Do NOT make potentially dangerous changes (e.g. pushing to main, deleting a repository) unless explicitly asked to do so.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
</VERSION_CONTROL>
|
||||
|
||||
<PROBLEM_SOLVING_WORKFLOW>
|
||||
1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions
|
||||
2. ANALYSIS: Consider multiple approaches and select the most promising one
|
||||
3. TESTING:
|
||||
* For bug fixes: Create tests to verify issues before implementing fixes
|
||||
* For new features: Consider test-driven development when appropriate
|
||||
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
|
||||
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
|
||||
5. VERIFICATION: Test your implementation thoroughly, including edge cases
|
||||
</PROBLEM_SOLVING_WORKFLOW>
|
||||
|
||||
<SECURITY>
|
||||
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
|
||||
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
|
||||
* If you've made repeated attempts to solve a problem, but the tests won't pass or the user says it's still broken, reflect on 5-7 different possible sources of the problem. Assess the likelihood of these options, and proceed with fixing the most likely one.
|
||||
</IMPORTANT>
|
||||
</SECURITY>
|
||||
|
||||
<ENVIRONMENT_SETUP>
|
||||
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
|
||||
* If you encounter missing dependencies:
|
||||
1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.)
|
||||
2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.)
|
||||
3. Only install individual packages directly if no dependency files are found or if only specific packages are needed
|
||||
* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible.
|
||||
</ENVIRONMENT_SETUP>
|
||||
|
||||
<TROUBLESHOOTING>
|
||||
* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken:
|
||||
1. Step back and reflect on 5-7 different possible sources of the problem
|
||||
2. Assess the likelihood of each possible cause
|
||||
3. Methodically address the most likely causes, starting with the highest probability
|
||||
4. Document your reasoning process
|
||||
* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
|
||||
</TROUBLESHOOTING>
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
|
||||
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
|
||||
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
|
||||
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.
|
||||
_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
|
||||
|
||||
### Command Execution
|
||||
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
|
||||
* Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands.
|
||||
* Timeout: Commands have a soft timeout of 120 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details)
|
||||
|
||||
### Running and Interacting with Processes
|
||||
* Long running commands: For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`.
|
||||
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, you can:
|
||||
- Send empty `command` to retrieve additional logs
|
||||
- Send text (set `command` to the text) to STDIN of the running process
|
||||
- Send control commands like `C-c` (Ctrl+C), `C-d` (Ctrl+D), or `C-z` (Ctrl+Z) to interrupt the process
|
||||
|
||||
### Best Practices
|
||||
* Directory verification: Before creating new directories or files, first verify the parent directory exists and is the correct location.
|
||||
* Directory management: Try to maintain working directory by using absolute paths and avoiding excessive use of `cd`.
|
||||
|
||||
### Output Handling
|
||||
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
|
||||
"""
|
||||
|
||||
CmdRunTool = ChatCompletionToolParam(
|
||||
|
||||
@@ -7,10 +7,28 @@ _STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating a
|
||||
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
||||
* The `undo_edit` command will revert the last edit made to the file at `path`
|
||||
|
||||
Notes for using the `str_replace` command:
|
||||
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
||||
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
|
||||
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
|
||||
|
||||
Before using this tool:
|
||||
1. Use the view tool to understand the file's contents and context
|
||||
2. Verify the directory path is correct (only applicable when creating new files):
|
||||
- Use the view tool to verify the parent directory exists and is the correct location
|
||||
|
||||
When making edits:
|
||||
- Ensure the edit results in idiomatic, correct code
|
||||
- Do not leave the code in a broken state
|
||||
- Always use absolute file paths (starting with /)
|
||||
|
||||
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
|
||||
|
||||
1. EXACT MATCHING: The `old_str` parameter must match EXACTLY one or more consecutive lines from the file, including all whitespace and indentation. The tool will fail if `old_str` matches multiple locations or doesn't match exactly with the file content.
|
||||
|
||||
2. UNIQUENESS: The `old_str` must uniquely identify a single instance in the file:
|
||||
- Include sufficient context before and after the change point (3-5 lines recommended)
|
||||
- If not unique, the replacement will not be performed
|
||||
|
||||
3. REPLACEMENT: The `new_str` parameter should contain the edited lines that replace the `old_str`. Both strings must be different.
|
||||
|
||||
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
|
||||
"""
|
||||
|
||||
StrReplaceEditorTool = ChatCompletionToolParam(
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
## Introduction
|
||||
|
||||
This package contains definitions of micro-agents. A micro-agent is defined
|
||||
in the following structure:
|
||||
|
||||
```
|
||||
[AgentName]
|
||||
├── agent.yaml
|
||||
└── prompt.md
|
||||
```
|
||||
|
||||
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
|
||||
is loaded and rendered, and used together with `agent.yaml` to initialize a
|
||||
micro-agent.
|
||||
|
||||
Micro-agents can be used independently. You can also use `ManagerAgent` which knows
|
||||
how to coordinate the agents and collaboratively finish a task.
|
||||
@@ -1,2 +0,0 @@
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
@@ -1,3 +0,0 @@
|
||||
* `delegate` - send a task to another agent from the list provided. Arguments:
|
||||
* `agent` - the agent to which the task is delegated. MUST match a name in the list of agents provided.
|
||||
* `inputs` - a dictionary of input parameters to the agent, as specified in the list
|
||||
@@ -1,2 +0,0 @@
|
||||
* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
|
||||
* `outputs` - a dictionary representing the outputs of your task, if any
|
||||
@@ -1,3 +0,0 @@
|
||||
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
|
||||
* `content` - the thought to record
|
||||
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
|
||||
@@ -1,2 +0,0 @@
|
||||
* `read` - reads the content of a file. Arguments:
|
||||
* `path` - the path of the file to read
|
||||
@@ -1,2 +0,0 @@
|
||||
* `reject` - reject the task. Arguments:
|
||||
* `outputs` - a dictionary with only a `reason` attribute
|
||||
@@ -1,2 +0,0 @@
|
||||
* `run` - runs a command on the command line in a Linux shell. Arguments:
|
||||
* `command` - the command to run
|
||||
@@ -1,3 +0,0 @@
|
||||
* `write` - writes the content to a file. Arguments:
|
||||
* `path` - the path of the file to write
|
||||
* `content` - the content to write to the file
|
||||
@@ -1,5 +0,0 @@
|
||||
Your response MUST be in JSON format. It must be an object, and it must contain two fields:
|
||||
* `action`, which is one of the actions specified here
|
||||
* `args`, which is a map of key-value pairs, specifying the arguments for that action
|
||||
|
||||
You MUST NOT include any other text besides the JSON response
|
||||
@@ -1,4 +0,0 @@
|
||||
Here is a recent history of actions you've taken in service of this plan,
|
||||
as well as observations you've made. This only includes the MOST RECENT
|
||||
actions and observations--more may have happened before that.
|
||||
They are time-ordered, with your most recent action at the bottom.
|
||||
@@ -1,88 +0,0 @@
|
||||
from jinja2 import BaseLoader, Environment
|
||||
|
||||
from openhands.agenthub.micro.instructions import instructions
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
from openhands.io import json
|
||||
from openhands.llm.llm import LLM
|
||||
|
||||
|
||||
def parse_response(orig_response: str) -> Action:
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(orig_response)
|
||||
|
||||
# load the action from the dict
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
|
||||
def to_json(obj: object, **kwargs: dict) -> str:
|
||||
"""Serialize an object to str format"""
|
||||
return json.dumps(obj, **kwargs)
|
||||
|
||||
|
||||
class MicroAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
prompt = ''
|
||||
agent_definition: dict = {}
|
||||
|
||||
def history_to_json(
|
||||
self, history: list[Event], max_events: int = 20, **kwargs: dict
|
||||
) -> str:
|
||||
"""
|
||||
Serialize and simplify history to str format
|
||||
"""
|
||||
processed_history = []
|
||||
event_count = 0
|
||||
|
||||
for event in reversed(history):
|
||||
if event_count >= max_events:
|
||||
break
|
||||
processed_history.append(
|
||||
event_to_memory(event, self.llm.config.max_message_chars)
|
||||
)
|
||||
event_count += 1
|
||||
|
||||
# history is in reverse order, let's fix it
|
||||
processed_history.reverse()
|
||||
|
||||
# everything starts with a message
|
||||
# the first message is already in the prompt as the task
|
||||
# TODO: so we don't need to include it in the history
|
||||
|
||||
return json.dumps(processed_history, **kwargs)
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
super().__init__(llm, config)
|
||||
if 'name' not in self.agent_definition:
|
||||
raise ValueError('Agent definition must contain a name')
|
||||
self.prompt_template = Environment(loader=BaseLoader()).from_string(self.prompt)
|
||||
self.delegates = all_microagents.copy()
|
||||
del self.delegates[self.agent_definition['name']]
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
last_user_message, last_image_urls = state.get_current_user_intent()
|
||||
prompt = self.prompt_template.render(
|
||||
state=state,
|
||||
instructions=instructions,
|
||||
to_json=to_json,
|
||||
history_to_json=self.history_to_json,
|
||||
delegates=self.delegates,
|
||||
latest_user_message=last_user_message,
|
||||
)
|
||||
content: list[TextContent | ImageContent] = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and last_image_urls:
|
||||
content.append(ImageContent(image_urls=last_image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(message),
|
||||
)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
@@ -1,6 +0,0 @@
|
||||
name: CoderAgent
|
||||
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
|
||||
inputs:
|
||||
task: string
|
||||
summary: string
|
||||
outputs: {}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Task
|
||||
You are a software engineer. You've inherited an existing codebase, which you
|
||||
need to modify to complete this task:
|
||||
|
||||
{{ state.inputs.task }}
|
||||
|
||||
{% if state.inputs.summary %}
|
||||
Here's a summary of the codebase, as it relates to this task:
|
||||
|
||||
{{ state.inputs.summary }}
|
||||
{% endif %}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
Do NOT finish until you have completed the tasks.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ history_to_json(state.history, max_events=20) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
@@ -1,25 +0,0 @@
|
||||
## Introduction
|
||||
|
||||
CommitWriterAgent can help write git commit message. Example:
|
||||
|
||||
```bash
|
||||
WORKSPACE_MOUNT_PATH="`PWD`" \
|
||||
poetry run python openhands/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
|
||||
```
|
||||
|
||||
This agent is special in the sense that it doesn't need a task. Once called,
|
||||
it attempts to read all diff in the git staging area and write a good commit
|
||||
message.
|
||||
|
||||
## Future work
|
||||
|
||||
### Feedback loop
|
||||
|
||||
The commit message could be (optionally) shown to the customer or
|
||||
other agents, so that CommitWriterAgent could gather feedback to further
|
||||
improve the commit message.
|
||||
|
||||
### Task rejection
|
||||
|
||||
When the agent cannot compile a commit message (e.g. not git repository), it
|
||||
should reject the task with an explanation.
|
||||
@@ -1,6 +0,0 @@
|
||||
name: CommitWriterAgent
|
||||
description: "Write a git commit message for files in the git staging area"
|
||||
inputs: {}
|
||||
outputs:
|
||||
answer: string
|
||||
reason: string
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user