Compare commits

..

1 Commits

Author SHA1 Message Date
mamoodi f69bf1f9ff Release 0.27.0 2025-02-27 10:01:06 -05:00
165 changed files with 3786 additions and 6504 deletions
+12 -19
View File
@@ -54,22 +54,22 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Build and push app image
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --push
- name: Build app image
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --load
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --load
- name: Get hash in App Image
id: get_hash_in_app_image
run: |
# Lowercase the repository owner
export REPO_OWNER=${{ github.repository_owner }}
REPO_OWNER=$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]')
# Run the build script in the app image
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${{ env.REPO_OWNER }}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${REPO_OWNER}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
# Get the hash from the build script
hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
@@ -122,19 +122,16 @@ jobs:
run: make install-python-dependencies
- name: Create source distribution and Dockerfile
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Build and push runtime image ${{ matrix.base_image.image }}
if: github.event.pull_request.head.repo.fork != true
run: |
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} --push -t ${{ matrix.base_image.tag }}
./containers/build.sh -i runtime -o ${{ github.repository_owner }} --push -t ${{ matrix.base_image.tag }}
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: docker/build-push-action@v6
with:
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
tags: ghcr.io/all-hands-ai/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
context: containers/runtime
- name: Upload runtime image for fork
@@ -236,9 +233,6 @@ jobs:
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Run docker runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs
@@ -247,7 +241,8 @@ jobs:
# Install to be able to retry on failures for flaky tests
poetry run pip install pytest-rerunfailures
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
@@ -301,9 +296,6 @@ jobs:
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Run runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs
@@ -312,7 +304,8 @@ jobs:
# Install to be able to retry on failures for flaky tests
poetry run pip install pytest-rerunfailures
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
-55
View File
@@ -1,55 +0,0 @@
cff-version: 1.2.0
message: "If you use this software, please cite it using the following metadata."
title: "OpenHands: An Open Platform for AI Software Developers as Generalist Agents"
authors:
- family-names: Wang
given-names: Xingyao
- family-names: Li
given-names: Boxuan
- family-names: Song
given-names: Yufan
- family-names: Xu
given-names: Frank F.
- family-names: Tang
given-names: Xiangru
- family-names: Zhuge
given-names: Mingchen
- family-names: Pan
given-names: Jiayi
- family-names: Song
given-names: Yueqi
- family-names: Li
given-names: Bowen
- family-names: Singh
given-names: Jaskirat
- family-names: Tran
given-names: Hoang H.
- family-names: Li
given-names: Fuqiang
- family-names: Ma
given-names: Ren
- family-names: Zheng
given-names: Mingzhang
- family-names: Qian
given-names: Bill
- family-names: Shao
given-names: Yanjun
- family-names: Muennighoff
given-names: Niklas
- family-names: Zhang
given-names: Yizhe
- family-names: Hui
given-names: Binyuan
- family-names: Lin
given-names: Junyang
- family-names: Brennan
given-names: Robert
- family-names: Peng
given-names: Hao
- family-names: Ji
given-names: Heng
- family-names: Neubig
given-names: Graham
year: 2024
doi: "10.48550/arXiv.2407.16741"
url: "https://arxiv.org/abs/2407.16741"
+3 -10
View File
@@ -2,13 +2,12 @@
These are the procedures and guidelines on how issues are triaged in this repo by the maintainers.
## General
* All issues must be tagged with **enhancement**, **bug** or **troubleshooting/help**.
* Issues may be tagged with what it relates to (**agent quality**, **frontend**, **resolver**, etc.).
* Most issues must be tagged with **enhancement** or **bug**.
* Issues may be tagged with what it relates to (**backend**, **frontend**, **agent quality**, etc.).
## Severity
* **Low**: Minor issues or affecting single user.
* **Medium**: Affecting multiple users.
* **High**: High visibility issues or affecting many users.
* **Critical**: Affecting all users or potential security issues.
## Effort
@@ -19,14 +18,8 @@ These are the procedures and guidelines on how issues are triaged in this repo b
## Not Enough Information
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear.
* If an issue is unclear and the author does not provide more information or respond to a request,
the issue may be closed as **not planned** (Usually after a week).
* If an issue is unclear and the author does not provide more information or respond to a request, the issue may be closed as **not planned** (Usually after a week).
## Multiple Requests/Fixes in One Issue
* These issues will be narrowed down to one request/fix so the issue is more easily tracked and fixed.
* Issues may be broken down into multiple issues if required.
## Stale and Auto Closures
* In order to keep a maintainable backlog, issues that have no activity within 30 days are automatically marked as **Stale**.
* If issues marked as **Stale** continue to have no activity for 7 more days, they will automatically be closed as not planned.
* Issues may be reopened by maintainers if deemed important.
-68
View File
@@ -95,11 +95,6 @@ workspace_base = "./workspace"
# List of allowed file extensions for uploads
#file_uploads_allowed_extensions = [".*"]
# Whether to enable the default LLM summarizing condenser when no condenser is specified in config
# When true, a LLMSummarizingCondenserConfig will be used as the default condenser
# When false, a NoOpCondenserConfig (no summarization) will be used
#enable_default_condenser = true
#################################### LLM #####################################
# Configuration for LLM models (group name starts with 'llm')
# use 'llm' for the default LLM config
@@ -299,69 +294,6 @@ llm_config = 'gpt3'
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
#security_analyzer = ""
#################################### Condenser #################################
# Condensers control how conversation history is managed and compressed when
# the context grows too large. Each agent uses one condenser configuration.
##############################################################################
[condenser]
# The type of condenser to use. Available options:
# - "noop": No condensing, keeps full history (default)
# - "observation_masking": Keeps full event structure but masks older observations
# - "recent": Keeps only recent events and discards older ones
# - "llm": Uses an LLM to summarize conversation history
# - "amortized": Intelligently forgets older events while preserving important context
# - "llm_attention": Uses an LLM to prioritize most relevant context
type = "noop"
# Examples for each condenser type (uncomment and modify as needed):
# 1. NoOp Condenser - No additional settings needed
#type = "noop"
# 2. Observation Masking Condenser
#type = "observation_masking"
# Number of most-recent events where observations will not be masked
#attention_window = 100
# 3. Recent Events Condenser
#type = "recent"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum number of events to keep in history
#max_events = 100
# 4. LLM Summarizing Condenser
#type = "llm"
# Reference to an LLM config to use for summarization
#llm_config = "condenser"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering summarization
#max_size = 100
# 5. Amortized Forgetting Condenser
#type = "amortized"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering forgetting
#max_size = 100
# 6. LLM Attention Condenser
#type = "llm_attention"
# Reference to an LLM config to use for attention scoring
#llm_config = "condenser"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering attention mechanism
#max_size = 100
# Example of a custom LLM configuration for condensers that require an LLM
# If not provided, it falls back to the default LLM
#[llm.condenser]
#model = "gpt-4o"
#temperature = 0.1
#max_tokens = 1024
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options
@@ -385,11 +385,6 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `false`
- Description: Use host network
- `runtime_binding_address`
- Type: `str`
- Default: `127.0.0.1`
- 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
- `enable_auto_lint`
- Type: `bool`
-33
View File
@@ -84,36 +84,3 @@ docker run # ...
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="your-secret" \
```
## Daytona Runtime
Another option is using [Daytona](https://www.daytona.io/) as a runtime provider:
### Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.
3. Enter a name for your key and confirm the creation.
4. Once the key is generated, copy it.
### Step 2: Set Your API Key as an Environment Variable
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
```bash
export DAYTONA_API_KEY="<your-api-key>"
```
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
### Step 3: Run OpenHands Locally Using Docker
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
```bash
bash -i <(curl -sL https://get.daytona.io/openhands)
```
#### What This Command Does:
- Downloads the latest OpenHands release script.
- Runs the script in an interactive Bash session.
- Automatically pulls and runs the OpenHands container using Docker.
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)
+4
View File
@@ -5,6 +5,9 @@ export function Demo() {
const videoRef = React.useRef<HTMLVideoElement>(null);
return (
<div
style={{ paddingBottom: "10px", paddingTop: "10px", textAlign: "center" }}
>
<video
playsInline
autoPlay={true}
@@ -17,5 +20,6 @@ export function Demo() {
>
<source src="img/teaser.mp4" type="video/mp4"></source>
</video>
</div>
);
}
@@ -1,5 +1,6 @@
.demo {
width: 100%;
padding: 30px;
max-width: 800px;
text-align: center;
border-radius: 40px;
@@ -17,29 +17,6 @@ export function HomepageHeader() {
<p className="header-subtitle">{siteConfig.tagline}</p>
<div style={{
textAlign: 'center',
fontSize: '1.2rem',
maxWidth: '800px',
margin: '0 auto',
padding: '0rem 0rem 1rem'
}}>
<p style={{ margin: '0' }}>
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
call APIs, and yes-even copy code snippets from StackOverflow.
<br/>
<Link to="https://docs.all-hands.dev/modules/usage/installation"
style={{
textDecoration: 'underline',
display: 'inline-block',
marginTop: '0.5rem'
}}
>
Get started with OpenHands.
</Link>
</p>
</div>
<div align="center" className="header-links">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers" /></a>
@@ -50,9 +27,12 @@ export function HomepageHeader() {
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits" /></a>
<br/>
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation" /></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv" /></a>
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score" /></a>
</div>
<Demo />
</div>
</div>
);
+2 -3
View File
@@ -1,14 +1,14 @@
/* homepageHeader.css */
.homepage-header {
padding: 1rem 0;
height: 800px;
}
.header-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
padding: 2rem;
font-weight: 300;
width: 100%;
}
@@ -25,7 +25,6 @@
.header-subtitle {
font-size: 1.5rem;
margin: 0.5rem 0;
}
.header-links {
+12 -7
View File
@@ -2,7 +2,15 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import { HomepageHeader } from '../components/HomepageHeader/HomepageHeader';
import { translate } from '@docusaurus/Translate';
import { Demo } from "../components/Demo/Demo";
export function Header({ title, summary }): JSX.Element {
return (
<div>
<h1>{title}</h1>
<h2 style={{ fontSize: '3rem' }}>{summary}</h2>
</div>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
@@ -15,14 +23,11 @@ export default function Home(): JSX.Element {
})}
>
<HomepageHeader />
<div style={{ textAlign: 'center', padding: '1rem 0' }}>
<Demo />
</div>
<div style={{ textAlign: 'center', padding: '0.5rem 2rem 1.5rem' }}>
<div style={{ textAlign: 'center', padding: '2rem' }}>
<br />
<h2>Most Popular Links</h2>
<ul style={{ listStyleType: 'none'}}>
<li><a href="/modules/usage/Installation">How to Run OpenHands</a></li>
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>
-2
View File
@@ -24,7 +24,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
game = None
@@ -122,7 +121,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def get_config(
@@ -211,7 +210,6 @@ def process_instance(
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance=instance)
@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
# Configure visibility of unit tests to the Agent.
USE_UNIT_TESTS = os.environ.get('USE_UNIT_TESTS', 'false').lower() == 'true'
@@ -204,7 +203,7 @@ def process_instance(
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance=instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -31,7 +31,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': functools.partial(
@@ -275,7 +274,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
-2
View File
@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response(state: State) -> str:
@@ -400,7 +399,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -25,7 +25,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
# Only CodeActAgent can delegate to BrowsingAgent
SUPPORTED_AGENT_CLS = {'CodeActAgent'}
@@ -75,7 +74,6 @@ def process_instance(
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
@@ -35,7 +35,6 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
@@ -395,7 +394,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
EVALUATION_LLM = 'gpt-4-1106-preview'
@@ -282,7 +281,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance.data_files)
state: State | None = asyncio.run(
-2
View File
@@ -31,7 +31,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
DATASET_CACHE_DIR = os.path.join(os.path.dirname(__file__), 'data')
@@ -149,7 +148,6 @@ def process_instance(
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -26,7 +26,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -83,7 +82,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
config=config,
-2
View File
@@ -49,7 +49,6 @@ from openhands.events.action import (
MessageAction,
)
from openhands.events.observation import Observation
from openhands.utils.async_utils import call_async_from_sync
ACTION_FORMAT = """
<<FINAL_ANSWER||
@@ -215,7 +214,6 @@ Ok now its time to start solving the question. Good luck!
"""
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
config=config,
@@ -39,7 +39,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
IMPORT_HELPER = {
'python': [
@@ -233,7 +232,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
state: State | None = asyncio.run(
run_controller(
@@ -31,7 +31,6 @@ from openhands.events.action import (
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -207,7 +206,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -41,7 +41,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent', 'CodeActAgent'}
@@ -146,7 +145,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, obs = initialize_runtime(runtime)
task_str += (
-2
View File
@@ -35,7 +35,6 @@ from openhands.events.action import (
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response_mint(state: State, task: Task, task_config: dict[str, int]):
@@ -185,7 +184,6 @@ def process_instance(
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime)
state: State | None = asyncio.run(
@@ -43,7 +43,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
config = load_app_config()
@@ -235,7 +234,6 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Run the agent
@@ -29,7 +29,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -196,7 +195,6 @@ If the program uses some packages that are incompatible, please figure out alter
"""
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -288,7 +288,7 @@ def process_instance(
'model_patch': model_patch,
'instance_id': instance_id,
},
test_log_path=test_output_path,
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]
@@ -40,7 +40,6 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
@@ -372,32 +371,6 @@ def complete_runtime(
f'Failed to git config --global core.pager "": {str(obs)}',
)
# First check for any git repositories in subdirectories
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to find git repositories: {str(obs)}',
)
git_dirs = [p for p in obs.content.strip().split('\n') if p]
if git_dirs:
# Remove all .git directories in subdirectories
for git_dir in git_dirs:
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove git directory {git_dir}: {str(obs)}',
)
# add all files
action = CmdRunAction(command='git add -A')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
@@ -465,7 +438,6 @@ def process_instance(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
@@ -7,7 +7,7 @@ import os
import re
from dataclasses import dataclass
from enum import Enum, auto
from typing import Dict, List, Union
from typing import Dict, List, Optional, Union
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import BrowseInteractiveAction
@@ -133,7 +133,7 @@ def parse_content_to_elements(content: str) -> Dict[str, str]:
return elements
def find_matching_anchor(content: str, selector: str) -> str | None:
def find_matching_anchor(content: str, selector: str) -> Optional[str]:
"""Find the anchor ID that matches the given selector description"""
elements = parse_content_to_elements(content)
@@ -28,7 +28,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import BrowserOutputObservation, CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def get_config(
@@ -276,7 +275,7 @@ if __name__ == '__main__':
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
)
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
init_task_env(runtime, args.server_hostname, env_llm_config)
dependencies = load_dependencies(runtime)
@@ -27,7 +27,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -105,7 +104,6 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
@@ -37,7 +37,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'VisualBrowsingAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
@@ -160,8 +159,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, goal_image_urls = initialize_runtime(runtime)
initial_user_action = MessageAction(content=task_str, image_urls=goal_image_urls)
state: State | None = asyncio.run(
@@ -36,7 +36,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
@@ -145,7 +144,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str = initialize_runtime(runtime)
state: State | None = asyncio.run(
@@ -30,7 +30,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
FAKE_RESPONSES = {
'CodeActAgent': fake_user_response,
@@ -109,7 +108,6 @@ def process_instance(
# create sandbox and run the agent
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
test_class.initialize_runtime(runtime)
@@ -1,7 +1,6 @@
import { screen, within } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { TrajectoryActions } from "#/components/features/trajectory/trajectory-actions";
describe("TrajectoryActions", () => {
@@ -15,7 +14,7 @@ describe("TrajectoryActions", () => {
});
it("should render correctly", () => {
renderWithProviders(
render(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
@@ -26,11 +25,10 @@ describe("TrajectoryActions", () => {
const actions = screen.getByTestId("feedback-actions");
within(actions).getByTestId("positive-feedback");
within(actions).getByTestId("negative-feedback");
within(actions).getByTestId("export-trajectory");
});
it("should call onPositiveFeedback when positive feedback is clicked", async () => {
renderWithProviders(
render(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
@@ -45,7 +43,7 @@ describe("TrajectoryActions", () => {
});
it("should call onNegativeFeedback when negative feedback is clicked", async () => {
renderWithProviders(
render(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
@@ -60,7 +58,7 @@ describe("TrajectoryActions", () => {
});
it("should call onExportTrajectory when negative feedback is clicked", async () => {
renderWithProviders(
render(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
@@ -2,7 +2,6 @@ import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Messages } from "#/components/features/chat/messages";
import type { Message } from "#/message";
import { renderWithProviders } from "test-utils";
describe("File Operations Messages", () => {
it("should show success indicator for successful file read operation", () => {
@@ -17,7 +16,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -36,7 +35,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -55,7 +54,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -74,7 +73,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -0,0 +1,35 @@
import { screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { renderWithProviders } from "test-utils";
import { RuntimeSizeSelector } from "#/components/shared/modals/settings/runtime-size-selector";
const renderRuntimeSizeSelector = () =>
renderWithProviders(<RuntimeSizeSelector isDisabled={false} />);
describe("RuntimeSizeSelector", () => {
it("should show both runtime size options", () => {
renderRuntimeSizeSelector();
// The options are in the hidden select element
const select = screen.getByRole("combobox", { hidden: true });
expect(select).toHaveValue("1");
expect(select).toHaveDisplayValue("1x (2 core, 8G)");
expect(select.children).toHaveLength(3); // Empty option + 2 size options
});
it("should show the full description text for disabled options", async () => {
renderRuntimeSizeSelector();
// Click the button to open the dropdown
const button = screen.getByRole("button", {
name: "1x (2 core, 8G) SETTINGS_FORM$RUNTIME_SIZE_LABEL",
});
button.click();
// Wait for the dropdown to open and find the description text
const description = await screen.findByText(
"Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes.",
);
expect(description).toBeInTheDocument();
expect(description).toHaveClass("whitespace-normal", "break-words");
});
});
+2476 -2045
View File
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -7,47 +7,47 @@
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "2.7.4",
"@heroui/react": "2.6.14",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.2.0",
"@react-router/serve": "^7.2.0",
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@react-types/shared": "^3.27.0",
"@reduxjs/toolkit": "^2.6.0",
"@reduxjs/toolkit": "^2.5.1",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.7.0",
"@tanstack/react-query": "^5.66.11",
"@stripe/stripe-js": "^5.5.0",
"@tanstack/react-query": "^5.66.7",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.1",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.4.7",
"framer-motion": "^12.4.4",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-browser-languagedetector": "^8.0.3",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.23",
"jose": "^6.0.8",
"isbot": "^5.1.22",
"jose": "^5.10.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.225.1",
"posthog-js": "^1.219.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-icons": "^5.4.0",
"react-markdown": "^9.0.3",
"react-redux": "^9.2.0",
"react-router": "^7.2.0",
"react-router": "^7.1.5",
"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",
"tailwind-merge": "^3.0.1",
"vite": "^6.1.0",
"web-vitals": "^3.5.2",
"ws": "^8.18.1"
"ws": "^8.18.0"
},
"scripts": {
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
@@ -81,14 +81,14 @@
"devDependencies": {
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.50.1",
"@react-router/dev": "^7.2.0",
"@react-router/dev": "^7.1.5",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.66.1",
"@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.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
@@ -96,13 +96,13 @@
"@types/ws": "^8.5.14",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.0.7",
"@vitest/coverage-v8": "^3.0.6",
"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.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3",
@@ -113,10 +113,10 @@
"lint-staged": "^15.4.3",
"msw": "^2.6.6",
"postcss": "^8.5.2",
"prettier": "^3.5.3",
"stripe": "^17.7.0",
"prettier": "^3.5.1",
"stripe": "^17.5.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.2"
+1 -1
View File
@@ -174,7 +174,7 @@ class OpenHands {
code: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/api/keycloak/callback",
"/api/github/callback",
{
code,
},
Binary file not shown.
@@ -14,10 +14,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { TypingIndicator } from "./typing-indicator";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useWsClient } from "#/context/ws-client-provider";
import { Messages } from "./messages";
import { ChatSuggestions } from "./chat-suggestions";
import { ActionSuggestions } from "./action-suggestions";
@@ -37,7 +34,7 @@ function getEntryPoint(
}
export function ChatInterface() {
const { send, isLoadingMessages, status, pendingMessages } = useWsClient();
const { send, isLoadingMessages } = useWsClient();
const dispatch = useDispatch();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
@@ -57,9 +54,6 @@ export function ChatInterface() {
const params = useParams();
const { mutate: getTrajectory } = useGetTrajectory();
const isClientDisconnected = status === WsClientProviderStatus.DISCONNECTED;
const hasPendingMessages = pendingMessages.length > 0;
const handleSendMessage = async (content: string, files: File[]) => {
if (messages.length === 0) {
posthog.capture("initial_query_submitted", {
@@ -82,16 +76,7 @@ export function ChatInterface() {
const timestamp = new Date().toISOString();
const pending = true;
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
// Create the chat message
const chatMessage = createChatMessage(content, imageUrls, timestamp);
// Send or queue the message depending on connection status
send(chatMessage);
// Set agent state to RUNNING when a message is sent
send(generateAgentStateChangeEvent(AgentState.RUNNING));
send(createChatMessage(content, imageUrls, timestamp));
setMessageToSend(null);
};
@@ -146,20 +131,8 @@ export function ChatInterface() {
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
>
{isLoadingMessages && (
<div className="flex flex-col items-center gap-2">
<div className="flex justify-center">
<LoadingSpinner size="small" />
{isClientDisconnected && (
<div className="text-sm text-neutral-400">
Waiting for client to become ready...
{hasPendingMessages && (
<div className="text-xs text-neutral-500 mt-1">
{pendingMessages.length} message
{pendingMessages.length !== 1 ? "s" : ""} will be sent when
connected
</div>
)}
</div>
)}
</div>
)}
@@ -206,7 +179,7 @@ export function ChatInterface() {
onSubmit={handleSendMessage}
onStop={handleStop}
isDisabled={
// Allow input even when loading, but not during confirmation
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
@@ -57,19 +57,18 @@ export function ChatMessage({
onClick={handleCopyToClipboard}
mode={isCopy ? "copied" : "copy"}
/>
<div className="text-sm overflow-auto break-words">
<Markdown
components={{
code,
ul,
ol,
a: anchor,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
</div>
<Markdown
className="text-sm overflow-auto break-words"
components={{
code,
ul,
ol,
a: anchor,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
{children}
</article>
);
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Link } from "react-router";
import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import ArrowUp from "#/icons/angle-up-solid.svg?react";
@@ -10,8 +9,6 @@ import ArrowDown from "#/icons/angle-down-solid.svg?react";
import CheckCircle from "#/icons/check-circle-solid.svg?react";
import XCircle from "#/icons/x-circle-solid.svg?react";
import { cn } from "#/utils/utils";
import { useConfig } from "#/hooks/query/use-config";
import { BILLING_SETTINGS } from "#/utils/feature-flags";
interface ExpandableMessageProps {
id?: string;
@@ -26,7 +23,6 @@ export function ExpandableMessage({
type,
success,
}: ExpandableMessageProps) {
const { data: config } = useConfig();
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
const [headline, setHeadline] = useState("");
@@ -42,28 +38,6 @@ export function ExpandableMessage({
const statusIconClasses = "h-4 w-4 ml-2 inline";
if (
BILLING_SETTINGS() &&
config?.APP_MODE === "saas" &&
id === "STATUS$ERROR_LLM_OUT_OF_CREDITS"
) {
return (
<div className="flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 border-danger">
<div className="text-sm w-full">
<div className="font-bold text-danger">
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")}
</div>
<Link
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
to="/settings/billing"
>
{t("BILLING$CLICK_TO_TOP_UP")}
</Link>
</div>
</div>
);
}
return (
<div
className={cn(
@@ -123,18 +97,17 @@ export function ExpandableMessage({
)}
</div>
{(!headline || showDetails) && (
<div className="text-sm overflow-auto">
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
</div>
<Markdown
className="text-sm overflow-auto"
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
)}
</div>
</div>
@@ -9,24 +9,14 @@ import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";
const notificationStates = [
AgentState.AWAITING_USER_INPUT,
AgentState.FINISHED,
AgentState.AWAITING_USER_CONFIRMATION,
];
export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status, pendingMessages } = useWsClient();
const { notify } = useNotification();
const { status } = useWsClient();
const [statusMessage, setStatusMessage] = React.useState<string>("");
const hasPendingMessages = pendingMessages.length > 0;
const updateStatusMessage = () => {
let message = curStatusMessage.message || "";
@@ -55,46 +45,13 @@ export function AgentStatusBar() {
updateStatusMessage();
}, [curStatusMessage.id]);
// Handle window focus/blur
React.useEffect(() => {
if (typeof window === "undefined") return undefined;
const handleFocus = () => {
browserTab.stopNotification();
};
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
browserTab.stopNotification();
};
}, []);
React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
if (hasPendingMessages) {
setStatusMessage(
`Connecting... (${pendingMessages.length} pending message${pendingMessages.length !== 1 ? "s" : ""})`,
);
} else {
setStatusMessage("Connecting...");
}
setStatusMessage("Connecting...");
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
if (notificationStates.includes(curAgentState)) {
const message = t(AGENT_STATUS_MAP[curAgentState].message);
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
body: t(`Agent state changed to ${curAgentState}`),
playSound: true,
});
// Update browser tab if window exists and is not focused
if (typeof document !== "undefined" && !document.hasFocus()) {
browserTab.startNotification(message);
}
}
}
}, [curAgentState, status, pendingMessages.length, notify, t]);
}, [curAgentState]);
return (
<div className="flex flex-col items-center">
@@ -101,11 +101,9 @@ export function ConversationCard({
"h-auto w-fit rounded-xl border border-[#525252]",
)}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2 flex-1 min-w-0 overflow-hidden mr-2">
{isActive && (
<span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
)}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 w-full">
{isActive && <span className="w-2 h-2 bg-blue-500 rounded-full" />}
{titleMode === "edit" && (
<input
ref={inputRef}
@@ -121,8 +119,7 @@ export function ConversationCard({
{titleMode === "view" && (
<p
data-testid="conversation-card-title"
className="text-sm leading-6 font-semibold bg-transparent truncate overflow-hidden"
title={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
>
{title}
</p>
@@ -99,6 +99,7 @@ export function GitHubRepositorySelector({
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
@@ -113,6 +114,7 @@ export function GitHubRepositorySelector({
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import ExportIcon from "#/icons/export.svg?react";
@@ -15,27 +14,22 @@ export function TrajectoryActions({
onNegativeFeedback,
onExportTrajectory,
}: TrajectoryActionsProps) {
const { t } = useTranslation();
return (
<div data-testid="feedback-actions" className="flex gap-1">
<TrajectoryActionButton
testId="positive-feedback"
onClick={onPositiveFeedback}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_HELPFUL")}
/>
<TrajectoryActionButton
testId="negative-feedback"
onClick={onNegativeFeedback}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_NOT_HELPFUL")}
/>
<TrajectoryActionButton
testId="export-trajectory"
onClick={onExportTrajectory}
icon={<ExportIcon width={15} height={15} />}
tooltip={t("BUTTON$EXPORT_CONVERSATION")}
/>
</div>
);
@@ -1,19 +1,15 @@
import { Tooltip } from "@heroui/react";
interface TrajectoryActionButtonProps {
testId?: string;
onClick: () => void;
icon: React.ReactNode;
tooltip?: string;
}
export function TrajectoryActionButton({
testId,
onClick,
icon,
tooltip,
}: TrajectoryActionButtonProps) {
const button = (
return (
<button
type="button"
data-testid={testId}
@@ -23,14 +19,4 @@ export function TrajectoryActionButton({
{icon}
</button>
);
if (tooltip) {
return (
<Tooltip content={tooltip} closeDelay={100}>
{button}
</Tooltip>
);
}
return button;
}
@@ -0,0 +1,43 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
interface FormFieldsetProps {
id: string;
label: string;
items: { key: string; value: string }[];
defaultSelectedKey?: string;
isClearable?: boolean;
}
export function FormFieldset({
id,
label,
items,
defaultSelectedKey,
isClearable,
}: FormFieldsetProps) {
return (
<fieldset className="flex flex-col gap-2">
<label htmlFor={id} className="font-[500] text-[#A3A3A3] text-xs">
{label}
</label>
<Autocomplete
id={id}
name={id}
aria-label={label}
defaultSelectedKey={defaultSelectedKey}
isClearable={isClearable}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{items.map((item) => (
<AutocompleteItem key={item.key} value={item.key}>
{item.value}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}
@@ -0,0 +1,46 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface AgentInputProps {
isDisabled: boolean;
defaultValue: string;
agents: string[];
}
export function AgentInput({
isDisabled,
defaultValue,
agents,
}: AgentInputProps) {
const { t } = useTranslation();
return (
<fieldset data-testid="agent-selector" className="flex flex-col gap-2">
<label htmlFor="agent" className="font-[500] text-[#A3A3A3] text-xs">
{t(I18nKey.SETTINGS_FORM$AGENT_LABEL)}
</label>
<Autocomplete
isDisabled={isDisabled}
isRequired
id="agent"
aria-label="Agent"
data-testid="agent-input"
name="agent"
defaultSelectedKey={defaultValue}
isClearable={false}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{agents.map((agent) => (
<AutocompleteItem key={agent} value={agent}>
{agent}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}
@@ -0,0 +1,46 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface SecurityAnalyzerInputProps {
isDisabled: boolean;
defaultValue: string;
securityAnalyzers: string[];
}
export function SecurityAnalyzerInput({
isDisabled,
defaultValue,
securityAnalyzers,
}: SecurityAnalyzerInputProps) {
const { t } = useTranslation();
return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="security-analyzer"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t(I18nKey.SETTINGS_FORM$SECURITY_ANALYZER_LABEL)}
</label>
<Autocomplete
isDisabled={isDisabled}
id="security-analyzer"
name="security-analyzer"
aria-label="Security Analyzer"
defaultSelectedKey={defaultValue}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{securityAnalyzers.map((analyzer) => (
<AutocompleteItem key={analyzer} value={analyzer}>
{analyzer}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}
@@ -100,6 +100,7 @@ export function ModelSelector({
<AutocompleteItem
data-testid={`provider-item-${provider}`}
key={provider}
value={provider}
>
{mapProvider(provider)}
</AutocompleteItem>
@@ -109,7 +110,7 @@ export function ModelSelector({
{Object.keys(models)
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
<AutocompleteItem key={provider}>
<AutocompleteItem key={provider} value={provider}>
{mapProvider(provider)}
</AutocompleteItem>
))}
@@ -147,7 +148,9 @@ export function ModelSelector({
{models[selectedProvider || ""]?.models
.filter((model) => VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem key={model}>{model}</AutocompleteItem>
<AutocompleteItem key={model} value={model}>
{model}
</AutocompleteItem>
))}
</AutocompleteSection>
<AutocompleteSection title="Others">
@@ -157,6 +160,7 @@ export function ModelSelector({
<AutocompleteItem
data-testid={`model-item-${model}`}
key={model}
value={model}
>
{model}
</AutocompleteItem>
@@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { Select, SelectItem } from "@heroui/react";
import { I18nKey } from "#/i18n/declaration";
interface RuntimeSizeSelectorProps {
isDisabled: boolean;
defaultValue?: number;
}
export function RuntimeSizeSelector({
isDisabled,
defaultValue,
}: RuntimeSizeSelectorProps) {
const { t } = useTranslation();
return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="runtime-size"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t(I18nKey.SETTINGS_FORM$RUNTIME_SIZE_LABEL)}
</label>
<Select
data-testid="runtime-size"
id="runtime-size"
name="runtime-size"
defaultSelectedKeys={[String(defaultValue || 1)]}
selectedKeys={[String(defaultValue || 1)]}
isDisabled={isDisabled}
selectionMode="single"
disallowEmptySelection
aria-label={t(I18nKey.SETTINGS_FORM$RUNTIME_SIZE_LABEL)}
classNames={{
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}
>
<SelectItem key="1" value={1}>
1x (2 core, 8G)
</SelectItem>
<SelectItem
key="2"
value={2}
isDisabled
classNames={{
description:
"whitespace-normal break-words min-w-[300px] max-w-[300px]",
base: "min-w-[300px] max-w-[300px]",
}}
description="Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes."
>
2x (4 core, 16G)
</SelectItem>
</Select>
</fieldset>
);
}
+1 -65
View File
@@ -39,14 +39,6 @@ const isMessageAction = (
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
// Check if an event is an agent state changed observation
const isAgentStateEvent = (event: Record<string, unknown>): boolean =>
isOpenHandsEvent(event) &&
"type" in event &&
event.type === "observation" &&
"observation_id" in event &&
event.observation_id === "agent_state_changed";
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
@@ -57,21 +49,15 @@ interface UseWsClient {
isLoadingMessages: boolean;
events: Record<string, unknown>[];
send: (event: Record<string, unknown>) => void;
queueMessage: (event: Record<string, unknown>) => void;
pendingMessages: Record<string, unknown>[];
}
const WsClientContext = React.createContext<UseWsClient>({
status: WsClientProviderStatus.DISCONNECTED,
isLoadingMessages: true,
events: [],
pendingMessages: [],
send: () => {
throw new Error("not connected");
},
queueMessage: () => {
throw new Error("not connected");
},
});
interface WsClientProviderProps {
@@ -123,50 +109,26 @@ export function WsClientProvider({
WsClientProviderStatus.DISCONNECTED,
);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [pendingMessages, setPendingMessages] = React.useState<
Record<string, unknown>[]
>([]);
const [backendReady, setBackendReady] = React.useState(false);
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
const messageRateHandler = useRate({ threshold: 250 });
function queueMessage(event: Record<string, unknown>) {
setPendingMessages((prev) => [...prev, event]);
}
function send(event: Record<string, unknown>) {
if (!sioRef.current) {
EventLogger.error("WebSocket is not connected.");
queueMessage(event);
return;
}
if (!backendReady) {
// If backend is not ready yet, queue the message
EventLogger.info("Backend not ready, queueing message");
queueMessage(event);
return;
}
sioRef.current.emit("oh_action", event);
}
function handleConnect() {
setStatus(WsClientProviderStatus.CONNECTED);
// Don't send queued messages yet - wait for backend ready signal
}
function handleMessage(event: Record<string, unknown>) {
if (isOpenHandsEvent(event) && isMessageAction(event)) {
messageRateHandler.record(new Date().getTime());
}
// Check if this is a state change event indicating backend is ready
if (isAgentStateEvent(event)) {
setBackendReady(true);
}
setEvents((prevEvents) => [...prevEvents, event]);
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
@@ -177,7 +139,6 @@ export function WsClientProvider({
function handleDisconnect(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
setBackendReady(false);
const sio = sioRef.current;
if (!sio) {
return;
@@ -189,34 +150,11 @@ export function WsClientProvider({
function handleError(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
setBackendReady(false);
updateStatusWhenErrorMessagePresent(data);
}
// Watch for backend ready state and send queued messages when ready
React.useEffect(() => {
if (backendReady && pendingMessages.length > 0 && sioRef.current) {
// Backend is ready and we have pending messages
EventLogger.info(`Sending ${pendingMessages.length} queued messages`);
pendingMessages.forEach((event) => {
sioRef.current?.emit("oh_action", event);
});
// Also set the agent state to RUNNING if needed
const agentStateEvent = {
action: "change_agent_state",
args: { agent_state: "running" },
};
sioRef.current.emit("oh_action", agentStateEvent);
setPendingMessages([]);
}
}, [backendReady, pendingMessages.length]);
React.useEffect(() => {
lastEventRef.current = null;
setBackendReady(false);
}, [conversationId]);
React.useEffect(() => {
@@ -272,11 +210,9 @@ export function WsClientProvider({
status,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
pendingMessages,
send,
queueMessage,
}),
[status, messageRateHandler.isUnderThreshold, events, pendingMessages],
[status, messageRateHandler.isUnderThreshold, events],
);
return <WsClientContext value={value}>{children}</WsClientContext>;
@@ -20,7 +20,6 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
github_token: settings.github_token,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
};
-1
View File
@@ -20,7 +20,6 @@ const getSettingsQueryFn = async () => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
GITHUB_TOKEN_IS_SET: apiSettings.github_token_is_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
};
};
-56
View File
@@ -1,56 +0,0 @@
import { useCallback, useRef } from "react";
import notificationSound from "../assets/notification.mp3";
import { useCurrentSettings } from "../context/settings-context";
export const useNotification = () => {
const { settings } = useCurrentSettings();
const audioRef = useRef<HTMLAudioElement | undefined>(undefined);
// Initialize audio only in browser environment
if (typeof window !== "undefined" && !audioRef.current) {
audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5;
}
const notify = useCallback(
async (
title: string,
options?: NotificationOptions & { playSound?: boolean },
): Promise<Notification | undefined> => {
if (typeof window === "undefined") return undefined;
// Only play sound if:
// 1. Explicitly requested via playSound option
// 2. Sound notifications are enabled in settings
// 3. Audio is available
// 4. Not a settings-related notification
if (
options?.playSound === true && // Must be explicitly true
settings?.ENABLE_SOUND_NOTIFICATIONS &&
audioRef.current &&
!title.includes("BUTTON$") // Don't play for button/settings actions
) {
// Reset and play sound
audioRef.current.currentTime = 0;
audioRef.current.play().catch(() => {
// Ignore autoplay errors
});
}
if (Notification.permission === "default") {
await Notification.requestPermission();
}
if (Notification.permission === "granted") {
// Remove playSound from options before passing to Notification
const { playSound, ...notificationOptions } = options || {};
return new Notification(title, notificationOptions);
}
return undefined;
},
[settings?.ENABLE_SOUND_NOTIFICATIONS],
);
return { notify };
};
-315
View File
@@ -1,315 +0,0 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
APP$TITLE = "APP$TITLE",
BROWSER$TITLE = "BROWSER$TITLE",
BROWSER$EMPTY_MESSAGE = "BROWSER$EMPTY_MESSAGE",
SETTINGS$TITLE = "SETTINGS$TITLE",
PROJECT$START_NEW = "PROJECT$START_NEW",
PROJECT$NEW = "PROJECT$NEW",
ACCOUNT_SETTINGS$TITLE = "ACCOUNT_SETTINGS$TITLE",
WORKSPACE$TITLE = "WORKSPACE$TITLE",
WORKSPACE$TERMINAL_TAB_LABEL = "WORKSPACE$TERMINAL_TAB_LABEL",
TERMINAL$WAITING_FOR_CLIENT = "TERMINAL$WAITING_FOR_CLIENT",
CODE_EDITOR$FILE_SAVED_SUCCESSFULLY = "CODE_EDITOR$FILE_SAVED_SUCCESSFULLY",
CODE_EDITOR$SAVING_LABEL = "CODE_EDITOR$SAVING_LABEL",
CODE_EDITOR$SAVE_LABEL = "CODE_EDITOR$SAVE_LABEL",
CODE_EDITOR$OPTIONS = "CODE_EDITOR$OPTIONS",
CODE_EDITOR$FILE_SAVE_ERROR = "CODE_EDITOR$FILE_SAVE_ERROR",
CODE_EDITOR$EMPTY_MESSAGE = "CODE_EDITOR$EMPTY_MESSAGE",
FILE_SERVICE$SELECT_FILE_ERROR = "FILE_SERVICE$SELECT_FILE_ERROR",
FILE_SERVICE$UPLOAD_FILES_ERROR = "FILE_SERVICE$UPLOAD_FILES_ERROR",
FILE_SERVICE$LIST_FILES_ERROR = "FILE_SERVICE$LIST_FILES_ERROR",
FILE_SERVICE$SAVE_FILE_ERROR = "FILE_SERVICE$SAVE_FILE_ERROR",
SUGGESTIONS$INCREASE_TEST_COVERAGE = "SUGGESTIONS$INCREASE_TEST_COVERAGE",
SUGGESTIONS$AUTO_MERGE_PRS = "SUGGESTIONS$AUTO_MERGE_PRS",
SUGGESTIONS$FIX_README = "SUGGESTIONS$FIX_README",
SUGGESTIONS$CLEAN_DEPENDENCIES = "SUGGESTIONS$CLEAN_DEPENDENCIES",
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
WORKSPACE$PLANNER_TAB_LABEL = "WORKSPACE$PLANNER_TAB_LABEL",
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
VSCODE$OPEN = "VSCODE$OPEN",
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
FIX_README = "FIX_README",
CLEAN_DEPENDENCIES = "CLEAN_DEPENDENCIES",
CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_LABEL = "CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_LABEL",
LLM$PROVIDER = "LLM$PROVIDER",
LLM$SELECT_PROVIDER_PLACEHOLDER = "LLM$SELECT_PROVIDER_PLACEHOLDER",
API$KEY = "API$KEY",
API$DONT_KNOW_KEY = "API$DONT_KNOW_KEY",
BUTTON$SAVE = "BUTTON$SAVE",
BUTTON$CLOSE = "BUTTON$CLOSE",
BUTTON$RESET_TO_DEFAULTS = "BUTTON$RESET_TO_DEFAULTS",
MODAL$CONFIRM_RESET_TITLE = "MODAL$CONFIRM_RESET_TITLE",
MODAL$CONFIRM_RESET_MESSAGE = "MODAL$CONFIRM_RESET_MESSAGE",
MODAL$END_SESSION_TITLE = "MODAL$END_SESSION_TITLE",
MODAL$END_SESSION_MESSAGE = "MODAL$END_SESSION_MESSAGE",
BUTTON$END_SESSION = "BUTTON$END_SESSION",
BUTTON$CANCEL = "BUTTON$CANCEL",
LANGUAGE$LABEL = "LANGUAGE$LABEL",
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
GITHUB$GET_TOKEN = "GITHUB$GET_TOKEN",
COMMON$HERE = "COMMON$HERE",
ANALYTICS$ENABLE = "ANALYTICS$ENABLE",
GITHUB$TOKEN_INVALID = "GITHUB$TOKEN_INVALID",
BUTTON$DISCONNECT = "BUTTON$DISCONNECT",
GITHUB$CONFIGURE_REPOS = "GITHUB$CONFIGURE_REPOS",
COMMON$CLICK_FOR_INSTRUCTIONS = "COMMON$CLICK_FOR_INSTRUCTIONS",
LLM$SELECT_MODEL_PLACEHOLDER = "LLM$SELECT_MODEL_PLACEHOLDER",
LLM$MODEL = "LLM$MODEL",
CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER = "CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER",
CONFIGURATION$MODAL_TITLE = "CONFIGURATION$MODAL_TITLE",
CONFIGURATION$MODEL_SELECT_LABEL = "CONFIGURATION$MODEL_SELECT_LABEL",
CONFIGURATION$MODEL_SELECT_PLACEHOLDER = "CONFIGURATION$MODEL_SELECT_PLACEHOLDER",
CONFIGURATION$AGENT_SELECT_LABEL = "CONFIGURATION$AGENT_SELECT_LABEL",
CONFIGURATION$AGENT_SELECT_PLACEHOLDER = "CONFIGURATION$AGENT_SELECT_PLACEHOLDER",
CONFIGURATION$LANGUAGE_SELECT_LABEL = "CONFIGURATION$LANGUAGE_SELECT_LABEL",
CONFIGURATION$LANGUAGE_SELECT_PLACEHOLDER = "CONFIGURATION$LANGUAGE_SELECT_PLACEHOLDER",
CONFIGURATION$SECURITY_SELECT_LABEL = "CONFIGURATION$SECURITY_SELECT_LABEL",
CONFIGURATION$SECURITY_SELECT_PLACEHOLDER = "CONFIGURATION$SECURITY_SELECT_PLACEHOLDER",
CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL = "CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL",
CONFIGURATION$MODAL_SAVE_BUTTON_LABEL = "CONFIGURATION$MODAL_SAVE_BUTTON_LABEL",
CONFIGURATION$MODAL_RESET_BUTTON_LABEL = "CONFIGURATION$MODAL_RESET_BUTTON_LABEL",
STATUS$CONNECTED_TO_SERVER = "STATUS$CONNECTED_TO_SERVER",
PROJECT$NEW_PROJECT = "PROJECT$NEW_PROJECT",
BROWSER$SCREENSHOT = "BROWSER$SCREENSHOT",
TIME$MINUTES_AGO = "TIME$MINUTES_AGO",
TIME$HOURS_AGO = "TIME$HOURS_AGO",
TIME$DAYS_AGO = "TIME$DAYS_AGO",
SETTINGS_FORM$RUNTIME_SIZE_LABEL = "SETTINGS_FORM$RUNTIME_SIZE_LABEL",
CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE = "CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE",
CONFIGURATION$AGENT_LOADING = "CONFIGURATION$AGENT_LOADING",
CONFIGURATION$AGENT_RUNNING = "CONFIGURATION$AGENT_RUNNING",
CONFIGURATION$ERROR_FETCH_MODELS = "CONFIGURATION$ERROR_FETCH_MODELS",
CONFIGURATION$SETTINGS_NOT_FOUND = "CONFIGURATION$SETTINGS_NOT_FOUND",
CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE = "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE",
SESSION$SERVER_CONNECTED_MESSAGE = "SESSION$SERVER_CONNECTED_MESSAGE",
SESSION$SESSION_HANDLING_ERROR_MESSAGE = "SESSION$SESSION_HANDLING_ERROR_MESSAGE",
SESSION$SESSION_CONNECTION_ERROR_MESSAGE = "SESSION$SESSION_CONNECTION_ERROR_MESSAGE",
SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE = "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE",
EXPLORER$UPLOAD_ERROR_MESSAGE = "EXPLORER$UPLOAD_ERROR_MESSAGE",
EXPLORER$LABEL_DROP_FILES = "EXPLORER$LABEL_DROP_FILES",
EXPLORER$LABEL_WORKSPACE = "EXPLORER$LABEL_WORKSPACE",
EXPLORER$EMPTY_WORKSPACE_MESSAGE = "EXPLORER$EMPTY_WORKSPACE_MESSAGE",
EXPLORER$LOADING_WORKSPACE_MESSAGE = "EXPLORER$LOADING_WORKSPACE_MESSAGE",
EXPLORER$REFRESH_ERROR_MESSAGE = "EXPLORER$REFRESH_ERROR_MESSAGE",
EXPLORER$UPLOAD_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_SUCCESS_MESSAGE",
EXPLORER$NO_FILES_UPLOADED_MESSAGE = "EXPLORER$NO_FILES_UPLOADED_MESSAGE",
EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE",
EXPLORER$UPLOAD_UNEXPECTED_RESPONSE_MESSAGE = "EXPLORER$UPLOAD_UNEXPECTED_RESPONSE_MESSAGE",
EXPLORER$VSCODE_SWITCHING_MESSAGE = "EXPLORER$VSCODE_SWITCHING_MESSAGE",
EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE = "EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE",
LOAD_SESSION$MODAL_TITLE = "LOAD_SESSION$MODAL_TITLE",
LOAD_SESSION$MODAL_CONTENT = "LOAD_SESSION$MODAL_CONTENT",
LOAD_SESSION$RESUME_SESSION_MODAL_ACTION_LABEL = "LOAD_SESSION$RESUME_SESSION_MODAL_ACTION_LABEL",
LOAD_SESSION$START_NEW_SESSION_MODAL_ACTION_LABEL = "LOAD_SESSION$START_NEW_SESSION_MODAL_ACTION_LABEL",
FEEDBACK$MODAL_TITLE = "FEEDBACK$MODAL_TITLE",
FEEDBACK$MODAL_CONTENT = "FEEDBACK$MODAL_CONTENT",
FEEDBACK$EMAIL_LABEL = "FEEDBACK$EMAIL_LABEL",
FEEDBACK$CONTRIBUTE_LABEL = "FEEDBACK$CONTRIBUTE_LABEL",
FEEDBACK$SHARE_LABEL = "FEEDBACK$SHARE_LABEL",
FEEDBACK$CANCEL_LABEL = "FEEDBACK$CANCEL_LABEL",
FEEDBACK$EMAIL_PLACEHOLDER = "FEEDBACK$EMAIL_PLACEHOLDER",
FEEDBACK$PASSWORD_COPIED_MESSAGE = "FEEDBACK$PASSWORD_COPIED_MESSAGE",
FEEDBACK$GO_TO_FEEDBACK = "FEEDBACK$GO_TO_FEEDBACK",
FEEDBACK$PASSWORD = "FEEDBACK$PASSWORD",
FEEDBACK$INVALID_EMAIL_FORMAT = "FEEDBACK$INVALID_EMAIL_FORMAT",
FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",
INVARIANT$SETTINGS_LABEL = "INVARIANT$SETTINGS_LABEL",
INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL = "INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL",
INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL = "INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL",
INVARIANT$INVARIANT_ANALYZER_LABEL = "INVARIANT$INVARIANT_ANALYZER_LABEL",
INVARIANT$INVARIANT_ANALYZER_MESSAGE = "INVARIANT$INVARIANT_ANALYZER_MESSAGE",
INVARIANT$CLICK_TO_LEARN_MORE_LABEL = "INVARIANT$CLICK_TO_LEARN_MORE_LABEL",
INVARIANT$POLICY_LABEL = "INVARIANT$POLICY_LABEL",
INVARIANT$LOG_LABEL = "INVARIANT$LOG_LABEL",
INVARIANT$EXPORT_TRACE_LABEL = "INVARIANT$EXPORT_TRACE_LABEL",
INVARIANT$TRACE_EXPORTED_MESSAGE = "INVARIANT$TRACE_EXPORTED_MESSAGE",
INVARIANT$POLICY_UPDATED_MESSAGE = "INVARIANT$POLICY_UPDATED_MESSAGE",
INVARIANT$SETTINGS_UPDATED_MESSAGE = "INVARIANT$SETTINGS_UPDATED_MESSAGE",
CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE = "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE",
CHAT_INTERFACE$AGENT_INIT_MESSAGE = "CHAT_INTERFACE$AGENT_INIT_MESSAGE",
CHAT_INTERFACE$AGENT_RUNNING_MESSAGE = "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE",
CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE = "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE",
CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE = "CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE",
CHAT_INTERFACE$AGENT_PAUSED_MESSAGE = "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE",
LANDING$TITLE = "LANDING$TITLE",
LANDING$SUBTITLE = "LANDING$SUBTITLE",
LANDING$START_HELP = "LANDING$START_HELP",
LANDING$START_HELP_LINK = "LANDING$START_HELP_LINK",
SUGGESTIONS$HELLO_WORLD = "SUGGESTIONS$HELLO_WORLD",
SUGGESTIONS$TODO_APP = "SUGGESTIONS$TODO_APP",
SUGGESTIONS$HACKER_NEWS = "SUGGESTIONS$HACKER_NEWS",
LANDING$CHANGE_PROMPT = "LANDING$CHANGE_PROMPT",
GITHUB$CONNECT = "GITHUB$CONNECT",
GITHUB$NO_RESULTS = "GITHUB$NO_RESULTS",
GITHUB$ADD_MORE_REPOS = "GITHUB$ADD_MORE_REPOS",
GITHUB$YOUR_REPOS = "GITHUB$YOUR_REPOS",
GITHUB$PUBLIC_REPOS = "GITHUB$PUBLIC_REPOS",
DOWNLOAD$PREPARING = "DOWNLOAD$PREPARING",
DOWNLOAD$DOWNLOADING = "DOWNLOAD$DOWNLOADING",
DOWNLOAD$FOUND_FILES = "DOWNLOAD$FOUND_FILES",
DOWNLOAD$SCANNING = "DOWNLOAD$SCANNING",
DOWNLOAD$FILES_PROGRESS = "DOWNLOAD$FILES_PROGRESS",
DOWNLOAD$CANCEL = "DOWNLOAD$CANCEL",
ACTION$CONFIRM = "ACTION$CONFIRM",
ACTION$REJECT = "ACTION$REJECT",
BADGE$BETA = "BADGE$BETA",
AGENT$RESUME_TASK = "AGENT$RESUME_TASK",
AGENT$PAUSE_TASK = "AGENT$PAUSE_TASK",
TOS$ACCEPT = "TOS$ACCEPT",
TOS$TERMS = "TOS$TERMS",
USER$ACCOUNT_SETTINGS = "USER$ACCOUNT_SETTINGS",
JUPYTER$OUTPUT_LABEL = "JUPYTER$OUTPUT_LABEL",
BUTTON$STOP = "BUTTON$STOP",
LANDING$ATTACH_IMAGES = "LANDING$ATTACH_IMAGES",
LANDING$OPEN_REPO = "LANDING$OPEN_REPO",
LANDING$IMPORT_PROJECT = "LANDING$IMPORT_PROJECT",
LANDING$UPLOAD_ZIP = "LANDING$UPLOAD_ZIP",
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
LANDING$OR = "LANDING$OR",
SUGGESTIONS$TEST_COVERAGE = "SUGGESTIONS$TEST_COVERAGE",
SUGGESTIONS$AUTO_MERGE = "SUGGESTIONS$AUTO_MERGE",
CHAT_INTERFACE$AGENT_STOPPED_MESSAGE = "CHAT_INTERFACE$AGENT_STOPPED_MESSAGE",
CHAT_INTERFACE$AGENT_FINISHED_MESSAGE = "CHAT_INTERFACE$AGENT_FINISHED_MESSAGE",
CHAT_INTERFACE$AGENT_REJECTED_MESSAGE = "CHAT_INTERFACE$AGENT_REJECTED_MESSAGE",
CHAT_INTERFACE$AGENT_ERROR_MESSAGE = "CHAT_INTERFACE$AGENT_ERROR_MESSAGE",
CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE = "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE",
CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE = "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE",
CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE = "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE",
CHAT_INTERFACE$INPUT_PLACEHOLDER = "CHAT_INTERFACE$INPUT_PLACEHOLDER",
CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE = "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE",
CHAT_INTERFACE$USER_ASK_CONFIRMATION = "CHAT_INTERFACE$USER_ASK_CONFIRMATION",
CHAT_INTERFACE$USER_CONFIRMED = "CHAT_INTERFACE$USER_CONFIRMED",
CHAT_INTERFACE$USER_REJECTED = "CHAT_INTERFACE$USER_REJECTED",
CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT = "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT",
CHAT_INTERFACE$CHAT_MESSAGE_COPIED = "CHAT_INTERFACE$CHAT_MESSAGE_COPIED",
CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED = "CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED",
CHAT_INTERFACE$TOOLTIP_COPY_MESSAGE = "CHAT_INTERFACE$TOOLTIP_COPY_MESSAGE",
CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE = "CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE",
CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE = "CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE",
CHAT_INTERFACE$INITIAL_MESSAGE = "CHAT_INTERFACE$INITIAL_MESSAGE",
CHAT_INTERFACE$ASSISTANT = "CHAT_INTERFACE$ASSISTANT",
CHAT_INTERFACE$TO_BOTTOM = "CHAT_INTERFACE$TO_BOTTOM",
CHAT_INTERFACE$MESSAGE_ARIA_LABEL = "CHAT_INTERFACE$MESSAGE_ARIA_LABEL",
CHAT_INTERFACE$CHAT_CONVERSATION = "CHAT_INTERFACE$CHAT_CONVERSATION",
CHAT_INTERFACE$UNKNOWN_SENDER = "CHAT_INTERFACE$UNKNOWN_SENDER",
SECURITY_ANALYZER$UNKNOWN_RISK = "SECURITY_ANALYZER$UNKNOWN_RISK",
SECURITY_ANALYZER$LOW_RISK = "SECURITY_ANALYZER$LOW_RISK",
SECURITY_ANALYZER$MEDIUM_RISK = "SECURITY_ANALYZER$MEDIUM_RISK",
SECURITY_ANALYZER$HIGH_RISK = "SECURITY_ANALYZER$HIGH_RISK",
SETTINGS$MODEL_TOOLTIP = "SETTINGS$MODEL_TOOLTIP",
SETTINGS$AGENT_TOOLTIP = "SETTINGS$AGENT_TOOLTIP",
SETTINGS$LANGUAGE_TOOLTIP = "SETTINGS$LANGUAGE_TOOLTIP",
SETTINGS$DISABLED_RUNNING = "SETTINGS$DISABLED_RUNNING",
SETTINGS$API_KEY_PLACEHOLDER = "SETTINGS$API_KEY_PLACEHOLDER",
SETTINGS$CONFIRMATION_MODE = "SETTINGS$CONFIRMATION_MODE",
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
STATUS$CONTAINER_STARTED = "STATUS$CONTAINER_STARTED",
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
ACCOUNT_SETTINGS_MODAL$SAVE = "ACCOUNT_SETTINGS_MODAL$SAVE",
ACCOUNT_SETTINGS_MODAL$CLOSE = "ACCOUNT_SETTINGS_MODAL$CLOSE",
ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID = "ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID",
CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN = "CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN",
CONNECT_TO_GITHUB_MODAL$HERE = "CONNECT_TO_GITHUB_MODAL$HERE",
CONNECT_TO_GITHUB_MODAL$CONNECT = "CONNECT_TO_GITHUB_MODAL$CONNECT",
CONNECT_TO_GITHUB_MODAL$CLOSE = "CONNECT_TO_GITHUB_MODAL$CLOSE",
CONNECT_TO_GITHUB_BY_TOKEN_MODAL$BY_CONNECTING_YOU_AGREE = "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$BY_CONNECTING_YOU_AGREE",
CONNECT_TO_GITHUB_BY_TOKEN_MODAL$CONTINUE = "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$CONTINUE",
LOADING_PROJECT$LOADING = "LOADING_PROJECT$LOADING",
CUSTOM_INPUT$OPTIONAL_LABEL = "CUSTOM_INPUT$OPTIONAL_LABEL",
SETTINGS_FORM$CUSTOM_MODEL_LABEL = "SETTINGS_FORM$CUSTOM_MODEL_LABEL",
SETTINGS_FORM$BASE_URL_LABEL = "SETTINGS_FORM$BASE_URL_LABEL",
SETTINGS_FORM$API_KEY_LABEL = "SETTINGS_FORM$API_KEY_LABEL",
SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL = "SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL",
SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL = "SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL",
SETTINGS_FORM$AGENT_LABEL = "SETTINGS_FORM$AGENT_LABEL",
SETTINGS_FORM$SECURITY_ANALYZER_LABEL = "SETTINGS_FORM$SECURITY_ANALYZER_LABEL",
SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL = "SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL",
SETTINGS_FORM$SAVE_LABEL = "SETTINGS_FORM$SAVE_LABEL",
SETTINGS_FORM$CLOSE_LABEL = "SETTINGS_FORM$CLOSE_LABEL",
SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL = "SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL",
SETTINGS_FORM$CANCEL_LABEL = "SETTINGS_FORM$CANCEL_LABEL",
SETTINGS_FORM$END_SESSION_LABEL = "SETTINGS_FORM$END_SESSION_LABEL",
SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE = "SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE",
SETTINGS_FORM$ARE_YOU_SURE_LABEL = "SETTINGS_FORM$ARE_YOU_SURE_LABEL",
SETTINGS_FORM$ALL_INFORMATION_WILL_BE_DELETED_MESSAGE = "SETTINGS_FORM$ALL_INFORMATION_WILL_BE_DELETED_MESSAGE",
PROJECT_MENU_DETAILS_PLACEHOLDER$NEW_PROJECT_LABEL = "PROJECT_MENU_DETAILS_PLACEHOLDER$NEW_PROJECT_LABEL",
PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB = "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB",
PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED = "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED",
PROJECT_MENU_DETAILS$AGO_LABEL = "PROJECT_MENU_DETAILS$AGO_LABEL",
STATUS$ERROR_LLM_AUTHENTICATION = "STATUS$ERROR_LLM_AUTHENTICATION",
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
AGENT_ERROR$ACTION_TIMEOUT = "AGENT_ERROR$ACTION_TIMEOUT",
PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL",
PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL",
PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL",
PROJECT_MENU_CARD$OPEN = "PROJECT_MENU_CARD$OPEN",
ACTION_BUTTON$RESUME = "ACTION_BUTTON$RESUME",
ACTION_BUTTON$PAUSE = "ACTION_BUTTON$PAUSE",
BROWSER$SCREENSHOT_ALT = "BROWSER$SCREENSHOT_ALT",
ERROR_TOAST$CLOSE_BUTTON_LABEL = "ERROR_TOAST$CLOSE_BUTTON_LABEL",
FILE_EXPLORER$UPLOAD = "FILE_EXPLORER$UPLOAD",
FILE_EXPLORER$REFRESH_WORKSPACE = "FILE_EXPLORER$REFRESH_WORKSPACE",
FILE_EXPLORER$OPEN_WORKSPACE = "FILE_EXPLORER$OPEN_WORKSPACE",
FILE_EXPLORER$CLOSE_WORKSPACE = "FILE_EXPLORER$CLOSE_WORKSPACE",
ACTION_MESSAGE$RUN = "ACTION_MESSAGE$RUN",
ACTION_MESSAGE$RUN_IPYTHON = "ACTION_MESSAGE$RUN_IPYTHON",
ACTION_MESSAGE$READ = "ACTION_MESSAGE$READ",
ACTION_MESSAGE$EDIT = "ACTION_MESSAGE$EDIT",
ACTION_MESSAGE$WRITE = "ACTION_MESSAGE$WRITE",
ACTION_MESSAGE$BROWSE = "ACTION_MESSAGE$BROWSE",
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
SETTINGS$DESCRIPTION = "SETTINGS$DESCRIPTION",
SETTINGS$WARNING = "SETTINGS$WARNING",
SIDEBAR$SETTINGS = "SIDEBAR$SETTINGS",
SIDEBAR$DOCS = "SIDEBAR$DOCS",
SUGGESTIONS$ADD_DOCS = "SUGGESTIONS$ADD_DOCS",
SUGGESTIONS$ADD_DOCKERFILE = "SUGGESTIONS$ADD_DOCKERFILE",
STATUS$CONNECTED = "STATUS$CONNECTED",
BROWSER$NO_PAGE_LOADED = "BROWSER$NO_PAGE_LOADED",
USER$AVATAR_PLACEHOLDER = "USER$AVATAR_PLACEHOLDER",
ACCOUNT_SETTINGS$SETTINGS = "ACCOUNT_SETTINGS$SETTINGS",
ACCOUNT_SETTINGS$LOGOUT = "ACCOUNT_SETTINGS$LOGOUT",
SETTINGS_FORM$ADVANCED_OPTIONS_LABEL = "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
CONVERSATION$NO_CONVERSATIONS = "CONVERSATION$NO_CONVERSATIONS",
LANDING$SELECT_REPO = "LANDING$SELECT_REPO",
BUTTON$SEND = "BUTTON$SEND",
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL = "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL",
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
}
+1 -90
View File
@@ -3833,21 +3833,7 @@
"pt": "Ocorreu um erro ao conectar ao ambiente de execução. Por favor, atualize a página.",
"tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin."
},
"STATUS$ERROR_LLM_OUT_OF_CREDITS": {
"en": "You're out of OpenHands Credits",
"ja": "OpenHandsクレジットが不足しています",
"zh-CN": "您的OpenHands点数已用完",
"zh-TW": "您的OpenHands點數已用完",
"ko-KR": "OpenHands 크레딧이 소진되었습니다",
"no": "Du er tom for OpenHands-kreditter",
"it": "Hai esaurito i crediti OpenHands",
"pt": "Você está sem créditos OpenHands",
"es": "Te has quedado sin créditos de OpenHands",
"ar": "لقد نفدت رصيدك من OpenHands",
"fr": "Vous n'avez plus de crédits OpenHands",
"tr": "OpenHands kredileriniz tükendi",
"de": "Ihre OpenHands-Guthaben sind aufgebraucht"
},
"STATUS$ERROR_RUNTIME_DISCONNECTED": {
"en": "There was an error while connecting to the runtime. Please refresh the page.",
"zh-CN": "运行时已断开连接",
@@ -4178,21 +4164,6 @@
"es": "Navegando en la web",
"tr": "Web'de geziniyor"
},
"ACTION_MESSAGE$THINK": {
"en": "Thinking",
"zh-CN": "思考",
"zh-TW": "思考",
"ko-KR": "생각",
"ja": "考える",
"no": "Tenker",
"ar": "يفكر",
"de": "Denkt",
"fr": "Pensant",
"it": "Pensando",
"pt": "Pensando",
"es": "Pensando",
"tr": "Düşünüyor"
},
"OBSERVATION_MESSAGE$RUN": {
"en": "Ran a bash command",
"zh-CN": "运行",
@@ -4587,65 +4558,5 @@
"SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": {
"en": "Enable Memory Condenser",
"zh-TW": "啟用記憶體壓縮器"
},
"BUTTON$MARK_HELPFUL": {
"en": "Mark this solution as helpful",
"zh-CN": "标记此解决方案有帮助",
"zh-TW": "標記此解決方案有幫助",
"de": "Diese Lösung als hilfreich markieren",
"ko-KR": "이 해결책이 도움이 되었다고 표시",
"no": "Marker denne løsningen som nyttig",
"it": "Segna questa soluzione come utile",
"pt": "Marcar esta solução como útil",
"es": "Marcar esta solución como útil",
"ar": "وضع علامة على هذا الحل كمفيد",
"fr": "Marquer cette solution comme utile",
"tr": "Bu çözümü yararlı olarak işaretle",
"ja": "このソリューションが役立つと評価"
},
"BUTTON$MARK_NOT_HELPFUL": {
"en": "Mark this solution as not helpful",
"zh-CN": "标记此解决方案没有帮助",
"zh-TW": "標記此解決方案沒有幫助",
"de": "Diese Lösung als nicht hilfreich markieren",
"ko-KR": "이 해결책이 도움이 되지 않았다고 표시",
"no": "Marker denne løsningen som ikke nyttig",
"it": "Segna questa soluzione come non utile",
"pt": "Marcar esta solução como não útil",
"es": "Marcar esta solución como no útil",
"ar": "وضع علامة على هذا الحل كغير مفيد",
"fr": "Marquer cette solution comme non utile",
"tr": "Bu çözümü yararlı değil olarak işaretle",
"ja": "このソリューションが役立たないと評価"
},
"BUTTON$EXPORT_CONVERSATION": {
"en": "Export conversation",
"zh-CN": "导出对话",
"zh-TW": "導出對話",
"de": "Konversation exportieren",
"ko-KR": "대화 내보내기",
"no": "Eksporter samtale",
"it": "Esporta conversazione",
"pt": "Exportar conversa",
"es": "Exportar conversación",
"ar": "تصدير المحادثة",
"fr": "Exporter la conversation",
"tr": "Konuşmayı dışa aktar",
"ja": "会話をエクスポート"
},
"BILLING$CLICK_TO_TOP_UP": {
"en": "Add funds to Your Account",
"ja": "アカウントに資金を追加",
"zh-CN": "为您的账户充值",
"zh-TW": "為您的帳戶充值",
"ko-KR": "계정에 자금 추가",
"no": "Legg til midler på kontoen din",
"it": "Aggiungi fondi al tuo account",
"pt": "Adicionar fundos à sua conta",
"es": "Añadir fondos a tu cuenta",
"ar": "إضافة رصيد إلى حسابك",
"fr": "Ajouter des fonds à votre compte",
"tr": "Hesabınıza bakiye ekleyin",
"de": "Guthaben zu Ihrem Konto hinzufügen"
}
}
-1
View File
@@ -20,7 +20,6 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
github_token_is_set: DEFAULT_SETTINGS.GITHUB_TOKEN_IS_SET,
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,
};
-11
View File
@@ -104,8 +104,6 @@ function AccountSettings() {
formData.get("enable-analytics-switch")?.toString() === "on";
const enableMemoryCondenser =
formData.get("enable-memory-condenser-switch")?.toString() === "on";
const enableSoundNotifications =
formData.get("enable-sound-notifications-switch")?.toString() === "on";
saveSettings(
{
@@ -114,7 +112,6 @@ function AccountSettings() {
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:
@@ -401,14 +398,6 @@ function AccountSettings() {
>
Enable analytics
</SettingsSwitch>
<SettingsSwitch
testId="enable-sound-notifications-switch"
name="enable-sound-notifications-switch"
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
>
Enable sound notifications
</SettingsSwitch>
</section>
</div>
</form>
-22
View File
@@ -57,28 +57,6 @@ const messageActions = {
store.dispatch(appendJupyterInput(message.args.code));
}
},
[ActionType.FINISH]: (message: ActionMessage) => {
store.dispatch(addAssistantMessage(message.args.final_thought));
let successPrediction = "";
if (message.args.task_completed === "partial") {
successPrediction =
"The agent thinks that the task was **completed partially**.";
} else if (message.args.task_completed === "false") {
successPrediction =
"The agent thinks that the task was **not completed**.";
} else if (message.args.task_completed === "true") {
successPrediction =
"The agent thinks that the task was **completed successfully**.";
}
if (successPrediction) {
// if final_thought is not empty, add a new line before the success prediction
if (message.args.final_thought) {
store.dispatch(addAssistantMessage(`\n${successPrediction}`));
} else {
store.dispatch(addAssistantMessage(successPrediction));
}
}
},
};
export function handleActionMessage(message: ActionMessage) {
-13
View File
@@ -48,8 +48,6 @@ export function handleObservationMessage(message: ObservationMessage) {
break;
case ObservationType.READ:
case ObservationType.EDIT:
case ObservationType.THINK:
case ObservationType.NULL:
break; // We don't display the default message for these observations
default:
store.dispatch(addAssistantMessage(message.message));
@@ -89,16 +87,6 @@ export function handleObservationMessage(message: ObservationMessage) {
);
break;
case "read":
store.dispatch(
addAssistantObservation({
...baseObservation,
observation,
extras: {
path: String(message.extras.path || ""),
},
}),
);
break;
case "edit":
store.dispatch(
addAssistantObservation({
@@ -106,7 +94,6 @@ export function handleObservationMessage(message: ObservationMessage) {
observation,
extras: {
path: String(message.extras.path || ""),
diff: String(message.extras.diff || ""),
},
}),
);
-1
View File
@@ -13,7 +13,6 @@ export const DEFAULT_SETTINGS: Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
GITHUB_TOKEN_IS_SET: false,
ENABLE_DEFAULT_CONDENSER: true,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
};
+3 -10
View File
@@ -115,8 +115,6 @@ export const chatSlice = createSlice({
) {
text += `\n\n${getRiskText(action.payload.args.security_risk as unknown as ActionSecurityRisk)}`;
}
} else if (actionID === "think") {
text = action.payload.args.thought;
}
const message: Message = {
type: "action",
@@ -173,14 +171,9 @@ export const chatSlice = createSlice({
causeMessage.content
}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "read") {
causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI
} else if (observationID === "edit") {
if (causeMessage.success) {
causeMessage.content = `\`\`\`diff\n${observation.payload.extras.diff}\n\`\`\``; // Content is already truncated by the ACI
} else {
causeMessage.content = observation.payload.content;
}
} else if (observationID === "read" || observationID === "edit") {
const { content } = observation.payload;
causeMessage.content = `\`\`\`${observationID === "edit" ? "diff" : "python"}\n${content}\n\`\`\``; // Content is already truncated by the ACI
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {
-3
View File
@@ -26,9 +26,6 @@ enum ActionType {
// Delegate a (sub)task to another agent.
DELEGATE = "delegate",
// Logs a thought.
THINK = "think",
// If you're absolutely certain that you've completed your task and have tested your work,
// use the finish action to stop working.
FINISH = "finish",
-10
View File
@@ -41,18 +41,9 @@ export interface IPythonAction extends OpenHandsActionEvent<"run_ipython"> {
};
}
export interface ThinkAction extends OpenHandsActionEvent<"think"> {
source: "agent";
args: {
thought: string;
};
}
export interface FinishAction extends OpenHandsActionEvent<"finish"> {
source: "agent";
args: {
final_thought: string;
task_completed: "success" | "failure" | "partial";
outputs: Record<string, unknown>;
thought: string;
};
@@ -138,7 +129,6 @@ export type OpenHandsAction =
| AssistantMessageAction
| CommandAction
| IPythonAction
| ThinkAction
| FinishAction
| DelegateAction
| BrowseAction
-1
View File
@@ -10,7 +10,6 @@ export type OpenHandsEventType =
| "browse"
| "browse_interactive"
| "reject"
| "think"
| "finish"
| "error";
-10
View File
@@ -70,7 +70,6 @@ export interface EditObservation extends OpenHandsObservationEvent<"edit"> {
source: "agent";
extras: {
path: string;
diff: string;
};
}
@@ -81,17 +80,8 @@ export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
};
}
export interface AgentThinkObservation
extends OpenHandsObservationEvent<"think"> {
source: "agent";
extras: {
thought: string;
};
}
export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
| CommandObservation
| IPythonObservation
| DelegateObservation
-6
View File
@@ -22,12 +22,6 @@ enum ObservationType {
// Delegate result
DELEGATE = "delegate",
// A response to the agent's thought (usually a static message)
THINK = "think",
// A no-op observation
NULL = "null",
}
export default ObservationType;
-2
View File
@@ -9,7 +9,6 @@ export type Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
GITHUB_TOKEN_IS_SET: boolean;
ENABLE_DEFAULT_CONDENSER: boolean;
ENABLE_SOUND_NOTIFICATIONS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
};
@@ -24,7 +23,6 @@ export type ApiSettings = {
remote_runtime_resource_factor: number | null;
github_token_is_set: boolean;
enable_default_condenser: boolean;
enable_sound_notifications: boolean;
user_consents_to_analytics: boolean | null;
};
-57
View File
@@ -1,57 +0,0 @@
let originalTitle = "";
let titleInterval: number | undefined;
const isBrowser =
typeof window !== "undefined" && typeof document !== "undefined";
export const browserTab = {
startNotification(message: string) {
if (!isBrowser) return;
// Store original title if not already stored
if (!originalTitle) {
originalTitle = document.title;
}
// Clear any existing interval
if (titleInterval) {
this.stopNotification();
}
// Alternate between original title and notification message
titleInterval = window.setInterval(() => {
document.title =
document.title === originalTitle ? message : originalTitle;
}, 1000);
// Set favicon to indicate notification
const favicon = document.querySelector(
'link[rel="icon"]',
) as HTMLLinkElement;
if (favicon) {
favicon.href = favicon.href.includes("?notification")
? favicon.href
: `${favicon.href}?notification`;
}
},
stopNotification() {
if (!isBrowser) return;
if (titleInterval) {
window.clearInterval(titleInterval);
titleInterval = undefined;
}
if (originalTitle) {
document.title = originalTitle;
}
// Reset favicon
const favicon = document.querySelector(
'link[rel="icon"]',
) as HTMLLinkElement;
if (favicon) {
favicon.href = favicon.href.replace("?notification", "");
}
},
};
-10
View File
@@ -37,16 +37,6 @@ class EventLogger {
}
}
/**
* Log an info message
* @param info The info message
*/
static info(info: string) {
if (this.isDevMode) {
console.info(info);
}
}
/**
* Log an error message
* @param error The error message
@@ -5,11 +5,7 @@
* @returns The URL to redirect to for GitHub OAuth
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
const authUrl = requestUrl.hostname
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
.replace(/(^|\.)localhost$/, "auth.staging.all-hands.dev");
const scope = "openid email profile offline_access";
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
};
-38
View File
@@ -1,38 +0,0 @@
---
name: add_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
---
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.
Microagents are specialized prompts that provide context and capabilities for specific domains or tasks. They are activated by trigger words in the conversation and help the AI assistant understand what capabilities are available, how to use specific APIs or tools, what limitations exist, and how to handle common scenarios.
When creating a new microagent:
* Create a markdown file in `.openhands/microagents/` with an appropriate name (e.g., `github.md`, `google_workspace.md`)
* Include YAML frontmatter with metadata (name, type, version, agent, triggers)
* type is by DEFAULT knowledge
* version is DEFAULT 1.0.0
* agent is by DEFAULT CodeActAgent
* Document any credentials, environment variables, or API access needed
* Keep trigger words specific to avoid false activations
* Include error handling guidance and limitations
* Provide clear usage examples
* Keep the prompt focused and concise
For detailed information, see:
* [Microagents Overview](https://docs.all-hands.dev/modules/usage/prompting/microagents-overview)
* [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/github.md)
-55
View File
@@ -1,55 +0,0 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container
---
# Docker Installation and Usage Guide
## Installation on Debian/Ubuntu Systems
To install Docker on a Debian/Ubuntu system, follow these steps:
```bash
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up the stable repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update package index again
sudo apt-get update
# Install Docker Engine
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
```
## Starting Docker in Container Environments
If you're in a container environment without systemd (like this workspace), start Docker with:
```bash
# Start Docker daemon in the background
sudo dockerd > /tmp/docker.log 2>&1 &
# Wait for Docker to initialize
sleep 5
```
## Verifying Docker Installation
To verify Docker is working correctly, run the hello-world container:
```bash
sudo docker run hello-world
```
-50
View File
@@ -1,50 +0,0 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s
- kube
---
# Kubernetes Local Development with KIND
## KIND Installation and Setup
KIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.
IMPORTANT: Before you proceed with installation, make sure you have docker installed locally.
### Installation
To install KIND on a Debian/Ubuntu system:
```bash
# Download KIND binary
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
# Make it executable
chmod +x ./kind
# Move to a directory in your PATH
sudo mv ./kind /usr/local/bin/
```
To install kubectl:
```bash
# Download kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# Make it executable
chmod +x kubectl
# Move to a directory in your PATH
sudo mv ./kubectl /usr/local/bin/
```
### Creating a Cluster
Create a basic KIND cluster:
```bash
kind create cluster
```
@@ -9,13 +9,16 @@ from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import Message, TextContent
from openhands.core.message_utils import (
apply_prompt_caching,
events_to_messages,
)
from openhands.events.action import (
Action,
AgentFinishAction,
)
from openhands.llm.llm import LLM
from openhands.memory.condenser import Condenser
from openhands.memory.conversation_memory import ConversationMemory
from openhands.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
@@ -32,7 +35,7 @@ class CodeActAgent(Agent):
### Overview
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents' **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
The conceptual idea is illustrated below. At each turn, the agent can:
@@ -87,11 +90,8 @@ class CodeActAgent(Agent):
disabled_microagents=self.config.disabled_microagents,
)
# Create a ConversationMemory instance
self.conversation_memory = ConversationMemory(self.prompt_manager)
self.condenser = Condenser.from_config(self.config.condenser)
logger.debug(f'Using condenser: {type(self.condenser)}')
logger.debug(f'Using condenser: {self.condenser}')
def reset(self) -> None:
"""Resets the CodeAct Agent."""
@@ -168,21 +168,13 @@ class CodeActAgent(Agent):
if not self.prompt_manager:
raise Exception('Prompt Manager not instantiated.')
# Use conversation_memory to process events instead of calling events_to_messages directly
messages = self.conversation_memory.process_initial_messages(
with_caching=self.llm.is_caching_prompt_active()
)
messages: list[Message] = self._initial_messages()
# Condense the events from the state.
events = self.condenser.condensed_history(state)
logger.debug(
f'Processing {len(events)} events from a total of {len(state.history)} events'
)
messages = self.conversation_memory.process_events(
condensed_history=events,
initial_messages=messages,
messages += events_to_messages(
events,
max_message_chars=self.llm.config.max_message_chars,
vision_is_active=self.llm.vision_is_active(),
enable_som_visual_browsing=self.config.enable_som_visual_browsing,
@@ -191,10 +183,26 @@ class CodeActAgent(Agent):
messages = self._enhance_messages(messages)
if self.llm.is_caching_prompt_active():
self.conversation_memory.apply_prompt_caching(messages)
apply_prompt_caching(messages)
return messages
def _initial_messages(self) -> list[Message]:
"""Creates the initial messages (including the system prompt) for the LLM conversation."""
assert self.prompt_manager, 'Prompt Manager not instantiated.'
return [
Message(
role='system',
content=[
TextContent(
text=self.prompt_manager.get_system_message(),
cache_prompt=self.llm.is_caching_prompt_active(),
)
],
)
]
def _enhance_messages(self, messages: list[Message]) -> list[Message]:
"""Enhances the user message with additional context based on keywords matched.
@@ -208,7 +216,6 @@ class CodeActAgent(Agent):
results: list[Message] = []
is_first_message_handled = False
prev_role = None
for msg in messages:
if msg.role == 'user' and not is_first_message_handled:
@@ -224,16 +231,6 @@ class CodeActAgent(Agent):
if msg.role == 'user':
self.prompt_manager.enhance_message(msg)
# Add double newline between consecutive user messages
if prev_role == 'user' and len(msg.content) > 0:
# Find the first TextContent in the message to add newlines
for content_item in msg.content:
if isinstance(content_item, TextContent):
# If the previous message was also from a user, prepend two newlines to ensure separation
content_item.text = '\n\n' + content_item.text
break
results.append(msg)
prev_role = msg.role
return results
@@ -17,7 +17,6 @@ from openhands.agenthub.codeact_agent.tools import (
IPythonTool,
LLMBasedFileEditTool,
StrReplaceEditorTool,
ThinkTool,
WebReadTool,
)
from openhands.core.exceptions import (
@@ -28,7 +27,6 @@ from openhands.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
AgentThinkAction,
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
@@ -44,9 +42,7 @@ from openhands.events.tool import ToolCallMetadata
def combine_thought(action: Action, thought: str) -> Action:
if not hasattr(action, 'thought'):
return action
if thought and action.thought:
action.thought = f'{thought}\n{action.thought}'
elif thought:
if thought:
action.thought = thought
return action
@@ -75,11 +71,6 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
raise RuntimeError(
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
) from e
# ================================================
# CmdRunTool (Bash)
# ================================================
if tool_call.function.name == CmdRunTool['function']['name']:
if 'command' not in arguments:
raise FunctionCallValidationError(
@@ -88,10 +79,6 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
# convert is_input to boolean
is_input = arguments.get('is_input', 'false') == 'true'
action = CmdRunAction(command=arguments['command'], is_input=is_input)
# ================================================
# IPythonTool (Jupyter)
# ================================================
elif tool_call.function.name == IPythonTool['function']['name']:
if 'code' not in arguments:
raise FunctionCallValidationError(
@@ -103,19 +90,8 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
agent='BrowsingAgent',
inputs=arguments,
)
# ================================================
# AgentFinishAction
# ================================================
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
# ================================================
# LLMBasedFileEditTool (LLM-based file editor, deprecated)
# ================================================
action = AgentFinishAction()
elif tool_call.function.name == LLMBasedFileEditTool['function']['name']:
if 'path' not in arguments:
raise FunctionCallValidationError(
@@ -162,25 +138,12 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
impl_source=FileEditSource.OH_ACI,
**other_kwargs,
)
# ================================================
# AgentThinkAction
# ================================================
elif tool_call.function.name == ThinkTool['function']['name']:
action = AgentThinkAction(thought=arguments.get('thought', ''))
# ================================================
# BrowserTool
# ================================================
elif tool_call.function.name == BrowserTool['function']['name']:
if 'code' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "code" in tool call {tool_call.function.name}'
)
action = BrowseInteractiveAction(browser_actions=arguments['code'])
# ================================================
# WebReadTool (simplified browsing)
# ================================================
elif tool_call.function.name == WebReadTool['function']['name']:
if 'url' not in arguments:
raise FunctionCallValidationError(
@@ -220,7 +183,7 @@ def get_tools(
codeact_enable_llm_editor: bool = False,
codeact_enable_jupyter: bool = False,
) -> list[ChatCompletionToolParam]:
tools = [CmdRunTool, ThinkTool, FinishTool]
tools = [CmdRunTool, FinishTool]
if codeact_enable_browsing:
tools.append(WebReadTool)
tools.append(BrowserTool)
@@ -1,22 +0,0 @@
{% if repository_info %}
<REPOSITORY_INFO>
At the user's request, repository {{ repository_info.repo_name }} has been cloned to directory {{ repository_info.repo_directory }}.
</REPOSITORY_INFO>
{% endif %}
{% if repository_instructions -%}
<REPOSITORY_INSTRUCTIONS>
{{ repository_instructions }}
</REPOSITORY_INSTRUCTIONS>
{% endif %}
{% if runtime_info and runtime_info.available_hosts -%}
<RUNTIME_INFORMATION>
The user has access to the following hosts for accessing a web application,
each of which has a corresponding port:
{% for host, port in runtime_info.available_hosts.items() -%}
* {{ host }} (port {{ port }})
{% endfor %}
When starting a web server, use the corresponding ports. You should also
set any options to allow iframes and CORS requests, and allow the server to
be accessed from any host (e.g. 0.0.0.0).
</RUNTIME_INFORMATION>
{% endif %}
@@ -1,169 +0,0 @@
Here's a running example of how to perform a task with the provided tools.
--------------------- START OF EXAMPLE ---------------------
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
A: Sure! Let me first check the current directory:
<function=execute_bash>
<parameter=command>
pwd && ls
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
/workspace
openhands@runtime:~/workspace$
A: There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
<function=str_replace_editor>
<parameter=command>create</parameter>
<parameter=path>/workspace/app.py</parameter>
<parameter=file_text>
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
if __name__ == '__main__':
app.run(port=5000)
</parameter>
</function>
USER: EXECUTION RESULT of [str_replace_editor]:
File created successfully at: /workspace/app.py
A: I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<function=execute_bash>
<parameter=command>
python3 app.py > server.log 2>&1 &
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
[1] 121
[1]+ Exit 1 python3 app.py > server.log 2>&1
A: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<function=execute_bash>
<parameter=command>
cat server.log
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
A: Looks like the server crashed because the `flask` module is not installed. Let me install the `flask` module for you:
<function=execute_bash>
<parameter=command>
pip3 install flask
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
A: The `flask` module is installed successfully. Let me run the Python file again:
<function=execute_bash>
<parameter=command>
python3 app.py > server.log 2>&1 &
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
[1] 124
A:
Let me check the server log again:
<function=execute_bash>
<parameter=command>
cat server.log
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
A:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
A:
Sure! Let me stop the server first:
<function=execute_bash>
<parameter=command>
kill 124
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
[1]+ Terminated python3 app.py > server.log 2>&1
A:
Now let me display the numbers in a table format:
<function=str_replace_editor>
<parameter=command>str_replace</parameter>
<parameter=path>/workspace/app.py</parameter>
<parameter=old_str>return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'</parameter>
<parameter=new_str>return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'</parameter>
</function>
USER: EXECUTION RESULT of [str_replace_editor]:
The file /workspace/app.py has been edited. Here's the result of running `cat -n` on a snippet of /workspace/app.py:
3
4 @app.route('/')
5 def index():
6 numbers = list(range(1, 11))
7 return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8
9 if __name__ == '__main__':
10 app.run(port=5000)
Review the changes and make sure they are as expected. Edit the file again if necessary.
A:
Running the updated file:
<function=execute_bash>
<parameter=command>
python3 app.py > server.log 2>&1 &
</parameter>
</function>
USER: EXECUTION RESULT of [execute_bash]:
[1] 126
A:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<function=finish>
</function>
--------------------- END OF EXAMPLE ---------------------
Do NOT assume the environment is the same as in the example above.
--------------------- NEW TASK DESCRIPTION ---------------------
@@ -1,3 +0,0 @@
--------------------- END OF NEW TASK DESCRIPTION ---------------------
PLEASE follow the format strictly! PLEASE EMIT ONE AND ONLY ONE FUNCTION CALL PER MESSAGE.
@@ -1,8 +0,0 @@
{% for agent_info in triggered_agents %}
<EXTRA_INFO>
The following information has been included based on a keyword match for "{{ agent_info.trigger_word }}".
It may or may not be relevant to the user's request.
{{ agent_info.agent.content }}
</EXTRA_INFO>
{% endfor %}
@@ -4,7 +4,6 @@ from .finish import FinishTool
from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
from .str_replace_editor import StrReplaceEditorTool
from .think import ThinkTool
from .web_read import WebReadTool
__all__ = [
@@ -15,5 +14,4 @@ __all__ = [
'LLMBasedFileEditTool',
'StrReplaceEditorTool',
'WebReadTool',
'ThinkTool',
]
@@ -1,39 +1,11 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_FINISH_DESCRIPTION = """Signals the completion of the current task or conversation.
Use this tool when:
- You have successfully completed the user's requested task
- You cannot proceed further due to technical limitations or missing information
The message should include:
- A clear summary of actions taken and their results
- Any next steps for the user
- Explanation if you're unable to complete the task
- Any follow-up questions if more information is needed
The task_completed field should be set to True if you believed you have completed the task, and False otherwise.
"""
_FINISH_DESCRIPTION = """Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task."""
FinishTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='finish',
description=_FINISH_DESCRIPTION,
parameters={
'type': 'object',
'required': ['message', 'task_completed'],
'properties': {
'message': {
'type': 'string',
'description': 'Final message to send to the user',
},
'task_completed': {
'type': 'string',
'enum': ['true', 'false', 'partial'],
'description': 'Whether you have completed the task.',
},
},
},
),
)
@@ -1,27 +0,0 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_THINK_DESCRIPTION = """Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed.
Common use cases:
1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective.
2. After receiving test results, use this tool to brainstorm ways to fix failing tests.
3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs.
4. When designing a new feature, use this tool to think through architecture decisions and implementation details.
5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses.
The tool simply logs your thought process for better transparency and does not execute any code or make changes."""
ThinkTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='think',
description=_THINK_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'thought': {'type': 'string', 'description': 'The thought to log.'},
},
'required': ['thought'],
},
),
)
+2 -8
View File
@@ -227,8 +227,6 @@ class AgentController:
err_id = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
elif isinstance(e, litellm.InternalServerError):
err_id = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
elif isinstance(e, litellm.BadRequestError) and 'ExceededBudget' in str(e):
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
elif isinstance(e, RateLimitError):
await self.set_agent_state_to(AgentState.RATE_LIMITED)
return
@@ -248,9 +246,8 @@ class AgentController:
)
reported = RuntimeError(
'There was an unexpected error while running the agent. Please '
'report this error to the developers by opening an issue at '
'https://github.com/All-Hands-AI/OpenHands. Your session ID is '
f' {self.id}. Error type: {e.__class__.__name__}'
f'report this error to the developers. Your session ID is {self.id}. '
f'Error type: {e.__class__.__name__}'
)
if (
isinstance(e, litellm.AuthenticationError)
@@ -698,13 +695,10 @@ class AgentController:
except (ContextWindowExceededError, BadRequestError, OpenAIError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
# We have to rely on string-matching because LiteLLM doesn't consistently
# wrap the failure in a ContextWindowExceededError
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or 'input length and `max_tokens` exceed context limit' in error_str
or isinstance(e, ContextWindowExceededError)
):
if self.agent.config.enable_history_truncation:
+2 -14
View File
@@ -14,12 +14,7 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
from openhands.core.setup import (
create_agent,
create_controller,
create_runtime,
initialize_repository_for_runtime,
)
from openhands.core.setup import create_agent, create_controller, create_runtime
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import (
Action,
@@ -114,6 +109,7 @@ async def main(loop: asyncio.AbstractEventLoop):
sid=sid,
headless_mode=True,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
controller, _ = create_controller(agent, runtime, config)
@@ -169,14 +165,6 @@ async def main(loop: asyncio.AbstractEventLoop):
await runtime.connect()
# Initialize repository if needed
if config.sandbox.selected_repo:
initialize_repository_for_runtime(
runtime,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
if initial_user_action:
# If there's an initial user action, enqueue it and do not prompt again
event_stream.add_event(initial_user_action, EventSource.USER)
-1
View File
@@ -82,7 +82,6 @@ class AppConfig(BaseModel):
daytona_target: str = Field(default='us')
cli_multiline_input: bool = Field(default=False)
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
enable_default_condenser: bool = Field(default=True)
defaults_dict: ClassVar[dict] = {}
+6 -140
View File
@@ -1,8 +1,7 @@
from typing import Literal, cast
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
from pydantic import BaseModel, Field
from openhands.core import logger
from openhands.core.config.llm_config import LLMConfig
@@ -11,21 +10,17 @@ class NoOpCondenserConfig(BaseModel):
type: Literal['noop'] = Field('noop')
model_config = {'extra': 'forbid'}
class ObservationMaskingCondenserConfig(BaseModel):
"""Configuration for ObservationMaskingCondenser."""
type: Literal['observation_masking'] = Field('observation_masking')
attention_window: int = Field(
default=100,
default=10,
description='The number of most-recent events where observations will not be masked.',
ge=1,
)
model_config = {'extra': 'forbid'}
class RecentEventsCondenserConfig(BaseModel):
"""Configuration for RecentEventsCondenser."""
@@ -39,11 +34,9 @@ class RecentEventsCondenserConfig(BaseModel):
ge=0,
)
max_events: int = Field(
default=100, description='Maximum number of events to keep.', ge=1
default=10, description='Maximum number of events to keep.', ge=1
)
model_config = {'extra': 'forbid'}
class LLMSummarizingCondenserConfig(BaseModel):
"""Configuration for LLMCondenser."""
@@ -56,17 +49,13 @@ class LLMSummarizingCondenserConfig(BaseModel):
# at least one event by default, because the best guess is that it's the user task
keep_first: int = Field(
default=1,
description='Number of initial events to always keep in history.',
description='The number of initial events to condense.',
ge=0,
)
max_size: int = Field(
default=100,
description='Maximum size of the condensed history before triggering forgetting.',
ge=2,
default=10, description='Maximum number of events to keep.', ge=1
)
model_config = {'extra': 'forbid'}
class AmortizedForgettingCondenserConfig(BaseModel):
"""Configuration for AmortizedForgettingCondenser."""
@@ -85,8 +74,6 @@ class AmortizedForgettingCondenserConfig(BaseModel):
ge=0,
)
model_config = {'extra': 'forbid'}
class LLMAttentionCondenserConfig(BaseModel):
"""Configuration for LLMAttentionCondenser."""
@@ -108,10 +95,7 @@ class LLMAttentionCondenserConfig(BaseModel):
ge=0,
)
model_config = {'extra': 'forbid'}
# Type alias for convenience
CondenserConfig = (
NoOpCondenserConfig
| ObservationMaskingCondenserConfig
@@ -120,121 +104,3 @@ CondenserConfig = (
| AmortizedForgettingCondenserConfig
| LLMAttentionCondenserConfig
)
def condenser_config_from_toml_section(
data: dict, llm_configs: dict | None = None
) -> dict[str, CondenserConfig]:
"""
Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
For CondenserConfig, the handling is different since it's a union type. The type of condenser
is determined by the 'type' field in the section.
Example:
Parse condenser config like:
[condenser]
type = "noop"
For condensers that require an LLM config, you can specify the name of an LLM config:
[condenser]
type = "llm"
llm_config = "my_llm" # References [llm.my_llm] section
Args:
data: The TOML dictionary representing the [condenser] section.
llm_configs: Optional dictionary of LLMConfig objects keyed by name.
Returns:
dict[str, CondenserConfig]: A mapping where the key "condenser" corresponds to the configuration.
"""
# Initialize the result mapping
condenser_mapping: dict[str, CondenserConfig] = {}
# Process config
try:
# Determine which condenser type to use based on 'type' field
condenser_type = data.get('type', 'noop')
# Handle LLM config reference if needed
if (
condenser_type in ('llm', 'llm_attention')
and 'llm_config' in data
and isinstance(data['llm_config'], str)
):
llm_config_name = data['llm_config']
if llm_configs and llm_config_name in llm_configs:
# Replace the string reference with the actual LLMConfig object
data_copy = data.copy()
data_copy['llm_config'] = llm_configs[llm_config_name]
config = create_condenser_config(condenser_type, data_copy)
else:
logger.openhands_logger.warning(
f"LLM config '{llm_config_name}' not found for condenser. Using default LLMConfig."
)
# Create a default LLMConfig if the referenced one doesn't exist
data_copy = data.copy()
# Try to use the fallback 'llm' config
if llm_configs is not None:
data_copy['llm_config'] = llm_configs.get('llm')
config = create_condenser_config(condenser_type, data_copy)
else:
config = create_condenser_config(condenser_type, data)
condenser_mapping['condenser'] = config
except (ValidationError, ValueError) as e:
logger.openhands_logger.warning(
f'Invalid condenser configuration: {e}. Using NoOpCondenserConfig.'
)
# Default to NoOpCondenserConfig if config fails
config = NoOpCondenserConfig()
condenser_mapping['condenser'] = config
return condenser_mapping
# For backward compatibility
from_toml_section = condenser_config_from_toml_section
def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
"""
Create a CondenserConfig instance based on the specified type.
Args:
condenser_type: The type of condenser to create.
data: The configuration data.
Returns:
A CondenserConfig instance.
Raises:
ValueError: If the condenser type is unknown.
ValidationError: If the provided data fails validation for the condenser type.
"""
# Mapping of condenser types to their config classes
condenser_classes = {
'noop': NoOpCondenserConfig,
'observation_masking': ObservationMaskingCondenserConfig,
'recent': RecentEventsCondenserConfig,
'llm': LLMSummarizingCondenserConfig,
'amortized': AmortizedForgettingCondenserConfig,
'llm_attention': LLMAttentionCondenserConfig,
}
if condenser_type not in condenser_classes:
raise ValueError(f'Unknown condenser type: {condenser_type}')
# Create and validate the config using direct instantiation
# Explicitly handle ValidationError to provide more context
try:
config_class = condenser_classes[condenser_type]
# Use type casting to help mypy understand the return type
return cast(CondenserConfig, config_class(**data))
except ValidationError as e:
# Just re-raise with a more descriptive message, but don't try to pass the errors
# which can cause compatibility issues with different pydantic versions
raise ValueError(
f"Validation failed for condenser type '{condenser_type}': {e}"
)
+1 -1
View File
@@ -48,7 +48,7 @@ class LLMConfig(BaseModel):
reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models.
"""
model: str = Field(default='claude-3-7-sonnet-20250219')
model: str = Field(default='claude-3-5-sonnet-20241022')
api_key: SecretStr | None = Field(default=None)
base_url: str | None = Field(default=None)
api_version: str | None = Field(default=None)
+1 -3
View File
@@ -17,7 +17,6 @@ class SandboxConfig(BaseModel):
remote_runtime_api_timeout: The timeout for the remote runtime API requests.
enable_auto_lint: Whether to enable auto-lint.
use_host_network: Whether to use the host network.
runtime_binding_address: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
initialize_plugins: Whether to initialize plugins.
force_rebuild_runtime: Whether to force rebuild the runtime image.
runtime_extra_deps: The extra dependencies to install in the runtime image (typically used for evaluation).
@@ -61,7 +60,6 @@ class SandboxConfig(BaseModel):
default=False
) # once enabled, OpenHands would lint files after editing
use_host_network: bool = Field(default=False)
runtime_binding_address: str = Field(default='127.0.0.1')
runtime_extra_build_args: list[str] | None = Field(default=None)
initialize_plugins: bool = Field(default=True)
force_rebuild_runtime: bool = Field(default=False)
@@ -72,7 +70,7 @@ class SandboxConfig(BaseModel):
close_delay: int = Field(default=15)
remote_runtime_resource_factor: int = Field(default=1)
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: dict | None = Field(default=None)
docker_runtime_kwargs: str | None = Field(default=None)
selected_repo: str | None = Field(default=None)
model_config = {'extra': 'forbid'}
+3 -49
View File
@@ -12,11 +12,9 @@ import toml
from dotenv import load_dotenv
from pydantic import BaseModel, SecretStr, ValidationError
from openhands import __version__
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.config.condenser_config import condenser_config_from_toml_section
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
@@ -195,44 +193,6 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
# Re-raise ValueError from SandboxConfig.from_toml_section
raise ValueError('Error in [sandbox] section in config.toml')
# Process condenser section if present
if 'condenser' in toml_config:
try:
# Pass the LLM configs to the condenser config parser
condenser_mapping = condenser_config_from_toml_section(
toml_config['condenser'], cfg.llms
)
# Assign the default condenser configuration to the default agent configuration
if 'condenser' in condenser_mapping:
# Get the default agent config and assign the condenser config to it
default_agent_config = cfg.get_agent_config()
default_agent_config.condenser = condenser_mapping['condenser']
logger.openhands_logger.debug(
'Default condenser configuration loaded from config toml and assigned to default agent'
)
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse [condenser] config from toml, values have not been applied.\nError: {e}'
)
# If no condenser section is in toml but enable_default_condenser is True,
# set LLMSummarizingCondenserConfig as default
elif cfg.enable_default_condenser:
from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
# Get default agent config
default_agent_config = cfg.get_agent_config()
# Create default LLM summarizing condenser config
default_condenser = LLMSummarizingCondenserConfig(
llm_config=cfg.get_llm_config(), # Use default LLM config
)
# Set as default condenser
default_agent_config.condenser = default_condenser
logger.openhands_logger.debug(
'Default LLM summarizing condenser assigned to default agent (no condenser in config)'
)
# Process extended section if present
if 'extended' in toml_config:
try:
@@ -243,15 +203,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
)
# Check for unknown sections
known_sections = {
'core',
'extended',
'agent',
'llm',
'security',
'sandbox',
'condenser',
}
known_sections = {'core', 'extended', 'agent', 'llm', 'security', 'sandbox'}
for key in toml_config:
if key.lower() not in known_sections:
logger.openhands_logger.warning(f'Unknown section [{key}] in {toml_file}')
@@ -540,6 +492,8 @@ def parse_arguments() -> argparse.Namespace:
args = parser.parse_args()
if args.version:
from openhands import __version__
print(f'OpenHands version: {__version__}')
sys.exit(0)
+4 -47
View File
@@ -6,21 +6,15 @@ import sys
import traceback
from datetime import datetime
from types import TracebackType
from typing import Any, Literal, Mapping, TextIO
from typing import Any, Literal, Mapping
import litellm
from pythonjsonlogger.json import JsonFormatter
from termcolor import colored
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
DEBUG_LLM = os.getenv('DEBUG_LLM', 'False').lower() in ['true', '1', 'yes']
# Structured logs with JSON, disabled by default
LOG_JSON = os.getenv('LOG_JSON', 'False').lower() in ['true', '1', 'yes']
LOG_JSON_LEVEL_KEY = os.getenv('LOG_JSON_LEVEL_KEY', 'level')
# Configure litellm logging based on DEBUG_LLM
if DEBUG_LLM:
confirmation = input(
@@ -83,14 +77,7 @@ class StackInfoFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
if record.levelno >= logging.ERROR:
# LogRecord attributes are dynamically typed
# Capture the current stack trace as a string
stack = traceback.format_stack()
# Remove the last entries which are related to the logging machinery
stack = stack[:-3] # Adjust this number if needed
# Join the stack frames into a single string
stack_str = ''.join(stack)
setattr(record, 'stack_info', stack_str)
setattr(record, 'stack_info', True)
setattr(record, 'exc_info', sys.exc_info())
return True
@@ -300,36 +287,10 @@ def get_file_handler(
file_name = f'openhands_{timestamp}.log'
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
file_handler.setLevel(log_level)
if LOG_JSON:
file_handler.setFormatter(json_formatter())
else:
file_handler.setFormatter(file_formatter)
file_handler.setFormatter(file_formatter)
return file_handler
def json_formatter():
return JsonFormatter(
'{message}{levelname}',
style='{',
rename_fields={'levelname': LOG_JSON_LEVEL_KEY},
timestamp=True,
)
def json_log_handler(
level: int = logging.INFO,
_out: TextIO = sys.stdout,
) -> logging.Handler:
"""
Configure logger instance for structured logging as json lines.
"""
handler = logging.StreamHandler(_out)
handler.setLevel(level)
handler.setFormatter(json_formatter())
return handler
# Set up logging
logging.basicConfig(level=logging.ERROR)
@@ -367,11 +328,7 @@ if current_log_level == logging.DEBUG:
LOG_TO_FILE = True
openhands_logger.debug('DEBUG mode enabled.')
if LOG_JSON:
openhands_logger.addHandler(json_log_handler(current_log_level))
else:
openhands_logger.addHandler(get_console_handler(current_log_level))
openhands_logger.addHandler(get_console_handler(current_log_level))
openhands_logger.addFilter(SensitiveDataFilter(openhands_logger.name))
openhands_logger.propagate = False
openhands_logger.debug('Logging initialized')
+1 -12
View File
@@ -20,7 +20,6 @@ from openhands.core.setup import (
create_controller,
create_runtime,
generate_sid,
initialize_repository_for_runtime,
)
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import MessageAction, NullAction
@@ -30,7 +29,6 @@ from openhands.events.observation import AgentStateChangedObservation
from openhands.events.serialization import event_from_dict
from openhands.io import read_input, read_task
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
class FakeUserResponseFunc(Protocol):
@@ -99,17 +97,8 @@ async def run_controller(
sid=sid,
headless_mode=headless_mode,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
# Connect to the runtime
call_async_from_sync(runtime.connect)
# Initialize repository if needed
if config.sandbox.selected_repo:
initialize_repository_for_runtime(
runtime,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
event_stream = runtime.event_stream
+4 -11
View File
@@ -7,7 +7,6 @@ OpenHands uses its own `Message` class (`openhands/core/message.py`) which provi
## Class Structure
Our `Message` class (`openhands/core/message.py`):
```python
class Message(BaseModel):
role: Literal['user', 'system', 'assistant', 'tool']
@@ -23,14 +22,13 @@ class Message(BaseModel):
```
litellm's `Message` class (`litellm/types/utils.py`):
```python
class Message(OpenAIObject):
content: str | None
content: Optional[str]
role: Literal["assistant", "user", "system", "tool", "function"]
tool_calls: List[ChatCompletionMessageToolCall] | None
function_call: FunctionCall | None
audio: ChatCompletionAudioResponse | None = None
tool_calls: Optional[List[ChatCompletionMessageToolCall]]
function_call: Optional[FunctionCall]
audio: Optional[ChatCompletionAudioResponse] = None
```
## How It Works
@@ -38,7 +36,6 @@ class Message(OpenAIObject):
1. **Message Creation**: Our `Message` class is a Pydantic model that supports rich content (text and images) through its `content` field.
2. **Serialization**: The class uses Pydantic's `@model_serializer` to convert messages into dictionaries that litellm can understand. We have two serialization methods:
```python
def _string_serializer(self) -> dict:
# convert content to a single string
@@ -58,7 +55,6 @@ class Message(OpenAIObject):
```
The appropriate serializer is chosen based on the message's capabilities:
```python
@model_serializer
def serialize_model(self) -> dict:
@@ -68,13 +64,11 @@ class Message(OpenAIObject):
```
3. **Tool Call Handling**: Tool calls require special attention in serialization because:
- They need to work with litellm's API calls (which accept both dicts and objects)
- They need to be properly serialized for token counting
- They need to maintain compatibility with different LLM providers' formats
4. **litellm Integration**: When we pass our messages to `litellm.completion()`, litellm doesn't care about the message class type - it works with the dictionary representation. This works because:
- litellm's transformation code (e.g., `litellm/llms/anthropic/chat/transformation.py`) processes messages based on their structure, not their type
- our serialization produces dictionaries that match litellm's expected format
- litellm handles rich content by looking at the message structure, supporting both simple string content and lists of content items
@@ -84,7 +78,6 @@ class Message(OpenAIObject):
### Token Counting
To use litellm's token counter, we need to make sure that all message components (including tool calls) are properly serialized to dictionaries. This is because:
- litellm's token counter expects dictionary structures
- Tool calls need to be included in the token count
- Different providers may count tokens differently for structured content
+367
View File
@@ -1,7 +1,374 @@
from litellm import ModelResponse
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import ImageContent, Message, TextContent
from openhands.core.schema import ActionType
from openhands.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
IPythonRunCellAction,
MessageAction,
)
from openhands.events.event import Event
from openhands.events.observation import (
AgentCondensationObservation,
AgentDelegateObservation,
BrowserOutputObservation,
CmdOutputObservation,
FileEditObservation,
FileReadObservation,
IPythonRunCellObservation,
UserRejectObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.observation import Observation
from openhands.events.serialization.event import truncate_content
from openhands.llm.metrics import Metrics, TokenUsage
def events_to_messages(
events: list[Event],
max_message_chars: int | None = None,
vision_is_active: bool = False,
enable_som_visual_browsing: bool = False,
) -> list[Message]:
"""Converts a list of events into a list of messages that can be sent to the LLM.
Ensures that tool call actions are processed correctly in function calling mode.
Args:
events: A list of events to convert. Each event can be an Action or Observation.
max_message_chars: The maximum number of characters in the content of an event included in the prompt to the LLM.
Larger observations are truncated.
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included.
enable_som_visual_browsing: Whether to enable visual browsing for the SOM model.
"""
messages = []
pending_tool_call_action_messages: dict[str, Message] = {}
tool_call_id_to_message: dict[str, Message] = {}
for event in events:
# create a regular message from an event
if isinstance(event, Action):
messages_to_add = get_action_message(
action=event,
pending_tool_call_action_messages=pending_tool_call_action_messages,
vision_is_active=vision_is_active,
)
elif isinstance(event, Observation):
messages_to_add = get_observation_message(
obs=event,
tool_call_id_to_message=tool_call_id_to_message,
max_message_chars=max_message_chars,
vision_is_active=vision_is_active,
enable_som_visual_browsing=enable_som_visual_browsing,
)
else:
raise ValueError(f'Unknown event type: {type(event)}')
# Check pending tool call action messages and see if they are complete
_response_ids_to_remove = []
for (
response_id,
pending_message,
) in pending_tool_call_action_messages.items():
assert pending_message.tool_calls is not None, (
'Tool calls should NOT be None when function calling is enabled & the message is considered pending tool call. '
f'Pending message: {pending_message}'
)
if all(
tool_call.id in tool_call_id_to_message
for tool_call in pending_message.tool_calls
):
# If complete:
# -- 1. Add the message that **initiated** the tool calls
messages_to_add.append(pending_message)
# -- 2. Add the tool calls **results***
for tool_call in pending_message.tool_calls:
messages_to_add.append(tool_call_id_to_message[tool_call.id])
tool_call_id_to_message.pop(tool_call.id)
_response_ids_to_remove.append(response_id)
# Cleanup the processed pending tool messages
for response_id in _response_ids_to_remove:
pending_tool_call_action_messages.pop(response_id)
messages += messages_to_add
return messages
def get_action_message(
action: Action,
pending_tool_call_action_messages: dict[str, Message],
vision_is_active: bool = False,
) -> list[Message]:
"""Converts an action into a message format that can be sent to the LLM.
This method handles different types of actions and formats them appropriately:
1. For tool-based actions (AgentDelegate, CmdRun, IPythonRunCell, FileEdit) and agent-sourced AgentFinish:
- In function calling mode: Stores the LLM's response in pending_tool_call_action_messages
- In non-function calling mode: Creates a message with the action string
2. For MessageActions: Creates a message with the text content and optional image content
Args:
action: The action to convert. Can be one of:
- CmdRunAction: For executing bash commands
- IPythonRunCellAction: For running IPython code
- FileEditAction: For editing files
- FileReadAction: For reading files using openhands-aci commands
- BrowseInteractiveAction: For browsing the web
- AgentFinishAction: For ending the interaction
- MessageAction: For sending messages
pending_tool_call_action_messages: Dictionary mapping response IDs to their corresponding messages.
Used in function calling mode to track tool calls that are waiting for their results.
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included
Returns:
list[Message]: A list containing the formatted message(s) for the action.
May be empty if the action is handled as a tool call in function calling mode.
Note:
In function calling mode, tool-based actions are stored in pending_tool_call_action_messages
rather than being returned immediately. They will be processed later when all corresponding
tool call results are available.
"""
# create a regular message from an event
if isinstance(
action,
(
AgentDelegateAction,
IPythonRunCellAction,
FileEditAction,
FileReadAction,
BrowseInteractiveAction,
BrowseURLAction,
),
) or (isinstance(action, CmdRunAction) and action.source == 'agent'):
tool_metadata = action.tool_call_metadata
assert tool_metadata is not None, (
'Tool call metadata should NOT be None when function calling is enabled. Action: '
+ str(action)
)
llm_response: ModelResponse = tool_metadata.model_response
assistant_msg = getattr(llm_response.choices[0], 'message')
# Add the LLM message (assistant) that initiated the tool calls
# (overwrites any previous message with the same response_id)
logger.debug(
f'Tool calls type: {type(assistant_msg.tool_calls)}, value: {assistant_msg.tool_calls}'
)
pending_tool_call_action_messages[llm_response.id] = Message(
role=getattr(assistant_msg, 'role', 'assistant'),
# tool call content SHOULD BE a string
content=[TextContent(text=assistant_msg.content or '')]
if assistant_msg.content is not None
else [],
tool_calls=assistant_msg.tool_calls,
)
return []
elif isinstance(action, AgentFinishAction):
role = 'user' if action.source == 'user' else 'assistant'
# when agent finishes, it has tool_metadata
# which has already been executed, and it doesn't have a response
# when the user finishes (/exit), we don't have tool_metadata
tool_metadata = action.tool_call_metadata
if tool_metadata is not None:
# take the response message from the tool call
assistant_msg = getattr(tool_metadata.model_response.choices[0], 'message')
content = assistant_msg.content or ''
# save content if any, to thought
if action.thought:
if action.thought != content:
action.thought += '\n' + content
else:
action.thought = content
# remove the tool call metadata
action.tool_call_metadata = None
if role not in ('user', 'system', 'assistant', 'tool'):
raise ValueError(f'Invalid role: {role}')
return [
Message(
role=role, # type: ignore[arg-type]
content=[TextContent(text=action.thought)],
)
]
elif isinstance(action, MessageAction):
role = 'user' if action.source == 'user' else 'assistant'
content = [TextContent(text=action.content or '')]
if vision_is_active and action.image_urls:
content.append(ImageContent(image_urls=action.image_urls))
if role not in ('user', 'system', 'assistant', 'tool'):
raise ValueError(f'Invalid role: {role}')
return [
Message(
role=role, # type: ignore[arg-type]
content=content,
)
]
elif isinstance(action, CmdRunAction) and action.source == 'user':
content = [TextContent(text=f'User executed the command:\n{action.command}')]
return [
Message(
role='user', # Always user for CmdRunAction
content=content,
)
]
return []
def get_observation_message(
obs: Observation,
tool_call_id_to_message: dict[str, Message],
max_message_chars: int | None = None,
vision_is_active: bool = False,
enable_som_visual_browsing: bool = False,
) -> list[Message]:
"""Converts an observation into a message format that can be sent to the LLM.
This method handles different types of observations and formats them appropriately:
- CmdOutputObservation: Formats command execution results with exit codes
- IPythonRunCellObservation: Formats IPython cell execution results, replacing base64 images
- FileEditObservation: Formats file editing results
- FileReadObservation: Formats file reading results from openhands-aci
- AgentDelegateObservation: Formats results from delegated agent tasks
- ErrorObservation: Formats error messages from failed actions
- UserRejectObservation: Formats user rejection messages
In function calling mode, observations with tool_call_metadata are stored in
tool_call_id_to_message for later processing instead of being returned immediately.
Args:
obs: The observation to convert
tool_call_id_to_message: Dictionary mapping tool call IDs to their corresponding messages (used in function calling mode)
max_message_chars: The maximum number of characters in the content of an observation included in the prompt to the LLM
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included
enable_som_visual_browsing: Whether to enable visual browsing for the SOM model
Returns:
list[Message]: A list containing the formatted message(s) for the observation.
May be empty if the observation is handled as a tool response in function calling mode.
Raises:
ValueError: If the observation type is unknown
"""
message: Message
if isinstance(obs, CmdOutputObservation):
# if it doesn't have tool call metadata, it was triggered by a user action
if obs.tool_call_metadata is None:
text = truncate_content(
f'\nObserved result of command executed by user:\n{obs.to_agent_observation()}',
max_message_chars,
)
else:
text = truncate_content(obs.to_agent_observation(), max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
text = obs.content
# replace base64 images with a placeholder
splitted = text.split('\n')
for i, line in enumerate(splitted):
if '![image](data:image/png;base64,' in line:
splitted[i] = (
'![image](data:image/png;base64, ...) already displayed to user'
)
text = '\n'.join(splitted)
text = truncate_content(text, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, FileEditObservation):
text = truncate_content(str(obs), max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, FileReadObservation):
message = Message(
role='user', content=[TextContent(text=obs.content)]
) # Content is already truncated by openhands-aci
elif isinstance(obs, BrowserOutputObservation):
text = obs.get_agent_obs_text()
if (
obs.trigger_by_action == ActionType.BROWSE_INTERACTIVE
and obs.set_of_marks is not None
and len(obs.set_of_marks) > 0
and enable_som_visual_browsing
and vision_is_active
):
text += 'Image: Current webpage screenshot (Note that only visible portion of webpage is present in the screenshot. You may need to scroll to view the remaining portion of the web-page.)\n'
message = Message(
role='user',
content=[
TextContent(text=text),
ImageContent(image_urls=[obs.set_of_marks]),
],
)
else:
message = Message(
role='user',
content=[TextContent(text=text)],
)
elif isinstance(obs, AgentDelegateObservation):
text = truncate_content(
obs.outputs['content'] if 'content' in obs.outputs else '',
max_message_chars,
)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, ErrorObservation):
text = truncate_content(obs.content, max_message_chars)
text += '\n[Error occurred in processing last action]'
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, UserRejectObservation):
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
text += '\n[Last action has been rejected by the user]'
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, AgentCondensationObservation):
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
else:
# If an observation message is not returned, it will cause an error
# when the LLM tries to return the next message
raise ValueError(f'Unknown observation type: {type(obs)}')
# Update the message as tool response properly
if (tool_call_metadata := obs.tool_call_metadata) is not None:
tool_call_id_to_message[tool_call_metadata.tool_call_id] = Message(
role='tool',
content=message.content,
tool_call_id=tool_call_metadata.tool_call_id,
name=tool_call_metadata.function_name,
)
# No need to return the observation message
# because it will be added by get_action_message when all the corresponding
# tool calls in the SAME request are processed
return []
return [message]
def apply_prompt_caching(messages: list[Message]) -> None:
"""Applies caching breakpoints to the messages.
For new Anthropic API, we only need to mark the last user or tool message as cacheable.
"""
# NOTE: this is only needed for anthropic
for message in reversed(messages):
if message.role in ('user', 'tool'):
message.content[
-1
].cache_prompt = True # Last item inside the message content
break
def get_token_usage_for_event(event: Event, metrics: Metrics) -> TokenUsage | None:
"""
Returns at most one token usage record for the `model_response.id` in this event's

Some files were not shown because too many files have changed in this diff Show More