mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
25 Commits
fix-messag
...
fix-agent-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd7c1db423 | ||
|
|
145a4bdc81 | ||
|
|
c25701fc1b | ||
|
|
bec05943a0 | ||
|
|
2c5018f529 | ||
|
|
d596fd2782 | ||
|
|
21c2253634 | ||
|
|
b95d54020c | ||
|
|
0e54bab56a | ||
|
|
bb5817cb56 | ||
|
|
f109a2ad95 | ||
|
|
956b3b4ab7 | ||
|
|
66781fc0f6 | ||
|
|
143293db44 | ||
|
|
c26185daf0 | ||
|
|
16da353508 | ||
|
|
c21ddaf1f1 | ||
|
|
801b134c7f | ||
|
|
5b063cc11b | ||
|
|
b1a18d5330 | ||
|
|
38b5198c24 | ||
|
|
dba25f5c46 | ||
|
|
c59abb5305 | ||
|
|
bd9fc5551b | ||
|
|
d80c3767ae |
55
CITATION.cff
55
CITATION.cff
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 += (
|
||||
|
||||
@@ -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'
|
||||
@@ -465,7 +464,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)
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
4521
frontend/package-lock.json
generated
4521
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -174,7 +174,7 @@ class OpenHands {
|
||||
code: string,
|
||||
): Promise<GitHubAccessTokenResponse> {
|
||||
const { data } = await openHands.post<GitHubAccessTokenResponse>(
|
||||
"/api/keycloak/callback",
|
||||
"/api/github/callback",
|
||||
{
|
||||
code,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -123,18 +123,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>
|
||||
|
||||
@@ -22,11 +22,10 @@ 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 { status } = useWsClient();
|
||||
const { notify } = useNotification();
|
||||
|
||||
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
||||
const hasPendingMessages = pendingMessages.length > 0;
|
||||
|
||||
const updateStatusMessage = () => {
|
||||
let message = curStatusMessage.message || "";
|
||||
@@ -72,13 +71,7 @@ export function AgentStatusBar() {
|
||||
|
||||
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)) {
|
||||
@@ -94,7 +87,7 @@ export function AgentStatusBar() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [curAgentState, status, pendingMessages.length, notify, t]);
|
||||
}, [curAgentState, notify, t]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
43
frontend/src/components/shared/form-fieldset.tsx
Normal file
43
frontend/src/components/shared/form-fieldset.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
46
frontend/src/components/shared/inputs/agent-input.tsx
Normal file
46
frontend/src/components/shared/inputs/agent-input.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -257,7 +257,6 @@ export enum I18nKey {
|
||||
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",
|
||||
@@ -280,7 +279,6 @@ export enum I18nKey {
|
||||
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",
|
||||
@@ -308,8 +306,11 @@ export enum I18nKey {
|
||||
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$ENABLE_SOUND = "BUTTON$ENABLE_SOUND",
|
||||
BUTTON$DISABLE_SOUND = "BUTTON$DISABLE_SOUND",
|
||||
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
|
||||
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
|
||||
NOTIFICATION$SOUND_ENABLED = "NOTIFICATION$SOUND_ENABLED",
|
||||
NOTIFICATION$SOUND_DISABLED = "NOTIFICATION$SOUND_DISABLED",
|
||||
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
|
||||
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -89,16 +89,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 +96,6 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
observation,
|
||||
extras: {
|
||||
path: String(message.extras.path || ""),
|
||||
diff: String(message.extras.diff || ""),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -173,14 +173,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) {
|
||||
|
||||
@@ -51,8 +51,6 @@ export interface ThinkAction extends OpenHandsActionEvent<"think"> {
|
||||
export interface FinishAction extends OpenHandsActionEvent<"finish"> {
|
||||
source: "agent";
|
||||
args: {
|
||||
final_thought: string;
|
||||
task_completed: "success" | "failure" | "partial";
|
||||
outputs: Record<string, unknown>;
|
||||
thought: string;
|
||||
};
|
||||
|
||||
@@ -70,7 +70,6 @@ export interface EditObservation extends OpenHandsObservationEvent<"edit"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
path: string;
|
||||
diff: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import openhands
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
@@ -77,21 +76,14 @@ class CodeActAgent(Agent):
|
||||
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2, ensure_ascii=False).replace("\\n", "\n")}'
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(
|
||||
os.path.dirname(os.path.dirname(openhands.__file__)),
|
||||
'microagents',
|
||||
)
|
||||
if self.config.enable_prompt_extensions
|
||||
else None,
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
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."""
|
||||
@@ -222,8 +214,6 @@ class CodeActAgent(Agent):
|
||||
|
||||
# enhance the user message with additional context based on keywords matched
|
||||
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
|
||||
@@ -232,7 +222,6 @@ class CodeActAgent(Agent):
|
||||
# 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
|
||||
|
||||
|
||||
@@ -108,10 +108,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
# AgentFinishAction
|
||||
# ================================================
|
||||
elif tool_call.function.name == FinishTool['function']['name']:
|
||||
action = AgentFinishAction(
|
||||
final_thought=arguments.get('message', ''),
|
||||
task_completed=arguments.get('task_completed', None),
|
||||
)
|
||||
action = AgentFinishAction()
|
||||
|
||||
# ================================================
|
||||
# LLMBasedFileEditTool (LLM-based file editor, deprecated)
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -248,9 +248,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 +697,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -46,6 +46,7 @@ class AppConfig(BaseModel):
|
||||
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
|
||||
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
|
||||
input is read line by line. When enabled, input continues until /exit command.
|
||||
microagents_dir: Directory containing global microagents.
|
||||
"""
|
||||
|
||||
llms: dict[str, LLMConfig] = Field(default_factory=dict)
|
||||
@@ -82,7 +83,10 @@ 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)
|
||||
microagents_dir: str = Field(
|
||||
default='microagents',
|
||||
description='Directory containing global microagents',
|
||||
)
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -18,9 +18,9 @@ from openhands.core.schema import AgentState
|
||||
from openhands.core.setup import (
|
||||
create_agent,
|
||||
create_controller,
|
||||
create_memory,
|
||||
create_runtime,
|
||||
generate_sid,
|
||||
initialize_repository_for_runtime,
|
||||
)
|
||||
from openhands.events import EventSource, EventStreamSubscriber
|
||||
from openhands.events.action import MessageAction, NullAction
|
||||
@@ -30,7 +30,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,20 +98,12 @@ 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
|
||||
|
||||
|
||||
replay_events: list[Event] | None = None
|
||||
if config.replay_trajectory_path:
|
||||
logger.info('Trajectory replay is enabled')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -82,5 +82,8 @@ class ActionTypeSchema(BaseModel):
|
||||
SEND_PR: str = Field(default='send_pr')
|
||||
"""Send a PR to github."""
|
||||
|
||||
RECALL: str = Field(default='recall')
|
||||
"""Retrieves data from a file or other storage."""
|
||||
|
||||
|
||||
ActionType = ActionTypeSchema()
|
||||
|
||||
@@ -49,5 +49,8 @@ class ObservationTypeSchema(BaseModel):
|
||||
CONDENSE: str = Field(default='condense')
|
||||
"""Result of a condensation operation."""
|
||||
|
||||
RECALL: str = Field(default='recall')
|
||||
"""Result of a recall operation."""
|
||||
|
||||
|
||||
ObservationType = ObservationTypeSchema()
|
||||
|
||||
@@ -16,11 +16,13 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventStream
|
||||
from openhands.events.event import Event
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroAgent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
|
||||
|
||||
def create_runtime(
|
||||
@@ -28,19 +30,18 @@ def create_runtime(
|
||||
sid: str | None = None,
|
||||
headless_mode: bool = True,
|
||||
agent: Agent | None = None,
|
||||
selected_repository: str | None = None,
|
||||
github_token: SecretStr | None = None,
|
||||
) -> Runtime:
|
||||
"""Create a runtime for the agent to run on.
|
||||
|
||||
Args:
|
||||
config: The app config.
|
||||
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
|
||||
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
|
||||
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
|
||||
where we don't want to have the VSCode UI open, so it defaults to True.
|
||||
agent: (optional) The agent instance to use for configuring the runtime.
|
||||
|
||||
Returns:
|
||||
The created Runtime instance (not yet connected or initialized).
|
||||
config: The app config.
|
||||
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
|
||||
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
|
||||
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
|
||||
where we don't want to have the VSCode UI open, so it defaults to True.
|
||||
selected_repository: (optional) The GitHub repository to use.
|
||||
github_token: (optional) The GitHub token to use.
|
||||
"""
|
||||
# if sid is provided on the command line, use it as the name of the event stream
|
||||
# otherwise generate it on the basis of the configured jwt_secret
|
||||
@@ -74,30 +75,8 @@ def create_runtime(
|
||||
headless_mode=headless_mode,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'Runtime created with plugins: {[plugin.name for plugin in runtime.plugins]}'
|
||||
)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
return runtime
|
||||
|
||||
|
||||
def initialize_repository_for_runtime(
|
||||
runtime: Runtime,
|
||||
agent: Agent | None = None,
|
||||
selected_repository: str | None = None,
|
||||
github_token: SecretStr | None = None,
|
||||
) -> str | None:
|
||||
"""Initialize the repository for the runtime.
|
||||
|
||||
Args:
|
||||
runtime: The runtime to initialize the repository for.
|
||||
agent: (optional) The agent to load microagents for.
|
||||
selected_repository: (optional) The GitHub repository to use.
|
||||
github_token: (optional) The GitHub token to use.
|
||||
|
||||
Returns:
|
||||
The repository directory path if a repository was cloned, None otherwise.
|
||||
"""
|
||||
# clone selected repository if provided
|
||||
repo_directory = None
|
||||
github_token = (
|
||||
@@ -120,7 +99,44 @@ def initialize_repository_for_runtime(
|
||||
agent.prompt_manager.load_microagents(microagents)
|
||||
agent.prompt_manager.set_repository_info(selected_repository, repo_directory)
|
||||
|
||||
return repo_directory
|
||||
logger.debug(
|
||||
f'Runtime initialized with plugins: {[plugin.name for plugin in runtime.plugins]}'
|
||||
)
|
||||
|
||||
return runtime
|
||||
|
||||
|
||||
def create_memory(
|
||||
microagents_dir: str,
|
||||
agent: Agent,
|
||||
runtime: Runtime,
|
||||
event_stream: EventStream,
|
||||
selected_repository: str | None = None,
|
||||
) -> Memory:
|
||||
# If the agent config has disabled microagents, use them
|
||||
disabled_microagents = agent.config.disabled_microagents
|
||||
|
||||
mem = Memory(
|
||||
event_stream=event_stream,
|
||||
microagents_dir=microagents_dir,
|
||||
disabled_microagents=disabled_microagents,
|
||||
)
|
||||
|
||||
if agent.prompt_manager and runtime:
|
||||
# sets available hosts
|
||||
mem.set_runtime_info(runtime.web_hosts)
|
||||
|
||||
# loads microagents from repo/.openhands/microagents
|
||||
microagents: list[BaseMicroAgent] = runtime.get_microagents_from_selected_repo(
|
||||
selected_repository
|
||||
)
|
||||
mem.load_user_workspace_microagents(microagents)
|
||||
|
||||
if selected_repository:
|
||||
repo_directory = selected_repository.split('/')[1]
|
||||
if repo_directory:
|
||||
mem.set_repository_info(selected_repository, repo_directory)
|
||||
return mem
|
||||
|
||||
|
||||
def create_agent(config: AppConfig) -> Agent:
|
||||
@@ -132,6 +148,7 @@ def create_agent(config: AppConfig) -> Agent:
|
||||
config=agent_config,
|
||||
)
|
||||
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from openhands.core.schema import ActionType
|
||||
@@ -34,26 +33,16 @@ class AgentSummarizeAction(Action):
|
||||
return ret
|
||||
|
||||
|
||||
class AgentFinishTaskCompleted(Enum):
|
||||
FALSE = 'false'
|
||||
PARTIAL = 'partial'
|
||||
TRUE = 'true'
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentFinishAction(Action):
|
||||
"""An action where the agent finishes the task.
|
||||
|
||||
Attributes:
|
||||
final_thought (str): The message to send to the user.
|
||||
task_completed (enum): Whether the agent believes the task has been completed.
|
||||
outputs (dict): The other outputs of the agent, for instance "content".
|
||||
outputs (dict): The outputs of the agent, for instance "content".
|
||||
thought (str): The agent's explanation of its actions.
|
||||
action (str): The action type, namely ActionType.FINISH.
|
||||
"""
|
||||
|
||||
final_thought: str = ''
|
||||
task_completed: AgentFinishTaskCompleted | None = None
|
||||
outputs: dict[str, Any] = field(default_factory=dict)
|
||||
thought: str = ''
|
||||
action: str = ActionType.FINISH
|
||||
@@ -106,3 +95,15 @@ class AgentDelegateAction(Action):
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I'm asking {self.agent} for help with this task."
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecallAction(Action):
|
||||
# This action is used for retrieving data, e.g., from memory or a knowledge base.
|
||||
query: dict[str, Any] = field(default_factory=dict)
|
||||
thought: str = ''
|
||||
action: str = ActionType.RECALL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Retrieved data for: {self.query}'
|
||||
|
||||
@@ -27,6 +27,13 @@ class AgentCondensationObservation(Observation):
|
||||
return self.content
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecallObservation(Observation):
|
||||
"""The output of a recall action."""
|
||||
|
||||
observation: str = ObservationType.RECALL
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentThinkObservation(Observation):
|
||||
"""The output of a think action.
|
||||
|
||||
@@ -62,12 +62,7 @@ class FileEditObservation(Observation):
|
||||
new_content: str | None = None
|
||||
observation: str = ObservationType.EDIT
|
||||
impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
|
||||
diff: str | None = (
|
||||
None # The raw diff between old and new content, used in OH_ACI mode
|
||||
)
|
||||
_diff_cache: str | None = (
|
||||
None # Cache for the diff visualization, used in LLM-based editing mode
|
||||
)
|
||||
_diff_cache: str | None = None # Cache for the diff visualization
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -131,7 +126,7 @@ class FileEditObservation(Observation):
|
||||
n_context_lines: int = 2,
|
||||
change_applied: bool = True,
|
||||
) -> str:
|
||||
"""Visualize the diff of the file edit. Used in the LLM-based editing mode.
|
||||
"""Visualize the diff of the file edit.
|
||||
|
||||
Instead of showing the diff line by line, this function shows each hunk
|
||||
of changes as a separate entity.
|
||||
|
||||
@@ -8,6 +8,7 @@ from openhands.events.action.agent import (
|
||||
AgentRejectAction,
|
||||
AgentThinkAction,
|
||||
ChangeAgentStateAction,
|
||||
RecallAction,
|
||||
)
|
||||
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
|
||||
from openhands.events.action.commands import (
|
||||
@@ -37,6 +38,7 @@ actions = (
|
||||
AgentDelegateAction,
|
||||
ChangeAgentStateAction,
|
||||
MessageAction,
|
||||
RecallAction,
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
|
||||
|
||||
@@ -27,6 +27,7 @@ class EventStreamSubscriber(str, Enum):
|
||||
RESOLVER = 'openhands_resolver'
|
||||
SERVER = 'server'
|
||||
RUNTIME = 'runtime'
|
||||
MEMORY = 'memory'
|
||||
MAIN = 'main'
|
||||
TEST = 'test'
|
||||
|
||||
|
||||
@@ -21,12 +21,7 @@ class GitHubService:
|
||||
token: SecretStr = SecretStr('')
|
||||
refresh = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str | None = None,
|
||||
idp_token: SecretStr | None = None,
|
||||
token: SecretStr | None = None,
|
||||
):
|
||||
def __init__(self, user_id: str | None = None, token: SecretStr | None = None):
|
||||
self.user_id = user_id
|
||||
|
||||
if token:
|
||||
@@ -51,9 +46,6 @@ class GitHubService:
|
||||
async def get_latest_token(self) -> SecretStr:
|
||||
return self.token
|
||||
|
||||
async def get_latest_provider_token(self) -> SecretStr:
|
||||
return self.token
|
||||
|
||||
async def _fetch_data(
|
||||
self, url: str, params: dict | None = None
|
||||
) -> tuple[Any, dict]:
|
||||
|
||||
@@ -76,8 +76,6 @@ REASONING_EFFORT_SUPPORTED_MODELS = [
|
||||
MODELS_WITHOUT_STOP_WORDS = [
|
||||
'o1-mini',
|
||||
'o1-preview',
|
||||
'o1',
|
||||
'o1-2024-12-17',
|
||||
]
|
||||
|
||||
|
||||
@@ -219,8 +217,9 @@ class LLM(RetryMixin, DebugMixin):
|
||||
kwargs['stop'] = STOP_WORDS
|
||||
|
||||
mock_fncall_tools = kwargs.pop('tools')
|
||||
# tool_choice should not be specified when mocking function calling
|
||||
kwargs.pop('tool_choice', None)
|
||||
kwargs['tool_choice'] = (
|
||||
'none' # force no tool calling because we're mocking it - without it, it will cause issue with sglang
|
||||
)
|
||||
|
||||
# if we have no messages, something went very wrong
|
||||
if not messages:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import openhands.memory.condenser.impl # noqa F401 (we import this to get the condensers registered)
|
||||
from openhands.memory.condenser.condenser import Condenser, get_condensation_metadata
|
||||
|
||||
__all__ = ['Condenser', 'get_condensation_metadata', 'CONDENSER_REGISTRY']
|
||||
__all__ = ['Condenser', 'get_condensation_metadata']
|
||||
|
||||
202
openhands/memory/memory.py
Normal file
202
openhands/memory/memory.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.agent import RecallAction
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.observation.agent import (
|
||||
RecallObservation,
|
||||
)
|
||||
from openhands.events.stream import EventStream, EventStreamSubscriber
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
|
||||
|
||||
|
||||
class Memory:
|
||||
"""
|
||||
Memory is a component that listens to the EventStream for either user MessageAction (to create
|
||||
a RecallAction) or a RecallAction (to produce a RecallObservation).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_stream: EventStream,
|
||||
microagents_dir: str,
|
||||
disabled_microagents: list[str] | None = None,
|
||||
):
|
||||
self.event_stream = event_stream
|
||||
self.microagents_dir = microagents_dir
|
||||
self.disabled_microagents = disabled_microagents or []
|
||||
# Subscribe to events
|
||||
self.event_stream.subscribe(
|
||||
EventStreamSubscriber.MEMORY,
|
||||
self.on_event,
|
||||
'Memory',
|
||||
)
|
||||
# Load global microagents (Knowledge + Repo).
|
||||
self._load_global_microagents()
|
||||
|
||||
# Additional placeholders to store user workspace microagents if needed
|
||||
self.repo_microagents: dict[str, RepoMicroAgent] = {}
|
||||
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
|
||||
|
||||
# Track whether we've seen the first user message
|
||||
self._first_user_message_seen = False
|
||||
|
||||
# Store repository / runtime info to send them to the templating later
|
||||
self.repository_info: RepositoryInfo | None = None
|
||||
self.runtime_info: RuntimeInfo | None = None
|
||||
|
||||
# TODO: enable_prompt_extensions
|
||||
|
||||
def _load_global_microagents(self) -> None:
|
||||
"""
|
||||
Loads microagents from the global microagents_dir.
|
||||
This is effectively what used to happen in PromptManager.
|
||||
"""
|
||||
repo_agents, knowledge_agents, _ = load_microagents_from_dir(
|
||||
self.microagents_dir
|
||||
)
|
||||
for name, agent in knowledge_agents.items():
|
||||
if name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(agent, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[name] = agent
|
||||
for name, agent in repo_agents.items():
|
||||
if name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(agent, RepoMicroAgent):
|
||||
self.repo_microagents[name] = agent
|
||||
|
||||
def set_repository_info(self, repo_name: str, repo_directory: str) -> None:
|
||||
"""Store repository info so we can reference it in an observation."""
|
||||
self.repository_info = RepositoryInfo(repo_name, repo_directory)
|
||||
self.prompt_manager.set_repository_info(self.repository_info)
|
||||
|
||||
def set_runtime_info(self, runtime_hosts: dict[str, int]) -> None:
|
||||
"""Store runtime info (web hosts, ports, etc.)."""
|
||||
# e.g. { '127.0.0.1': 8080 }
|
||||
self.runtime_info = RuntimeInfo(available_hosts=runtime_hosts)
|
||||
self.prompt_manager.set_runtime_info(self.runtime_info)
|
||||
|
||||
def on_event(self, event: Event):
|
||||
"""Handle an event from the event stream."""
|
||||
if isinstance(event, MessageAction):
|
||||
if event.source == 'user':
|
||||
# If this is the first user message, create and add a RecallObservation
|
||||
# with info about repo and runtime.
|
||||
if not self._first_user_message_seen:
|
||||
self._first_user_message_seen = True
|
||||
self._on_first_user_message(event)
|
||||
# continue with the next handler, to include microagents if suitable for this user message
|
||||
self._on_user_message_action(event)
|
||||
elif isinstance(event, RecallAction):
|
||||
self._on_recall_action(event)
|
||||
|
||||
def _on_first_user_message(self, event: MessageAction):
|
||||
"""Create and add to the stream a RecallObservation carrying info about repo and runtime."""
|
||||
# Build the same text that used to be appended to the first user message
|
||||
repo_instructions = ''
|
||||
assert (
|
||||
len(self.repo_microagents) <= 1
|
||||
), f'Expecting at most one repo microagent, but found {len(self.repo_microagents)}: {self.repo_microagents.keys()}'
|
||||
for microagent in self.repo_microagents.values():
|
||||
# We assume these are the repo instructions
|
||||
if repo_instructions:
|
||||
repo_instructions += '\n\n'
|
||||
repo_instructions += microagent.content
|
||||
|
||||
# Now wrap it in a RecallObservation, rather than altering the user message:
|
||||
obs = RecallObservation(
|
||||
content=self.prompt_manager.build_additional_info_text(repo_instructions)
|
||||
)
|
||||
self.event_stream.add_event(obs, EventSource.ENVIRONMENT)
|
||||
|
||||
def _on_user_message_action(self, event: MessageAction):
|
||||
"""Replicates old microagent logic: if a microagent triggers on user text,
|
||||
we embed it in an <extra_info> block and post a RecallObservation."""
|
||||
if event.source != 'user':
|
||||
return
|
||||
|
||||
# If there's no text, do nothing
|
||||
user_text = event.content.strip()
|
||||
if not user_text:
|
||||
return
|
||||
# Gather all triggered microagents
|
||||
microagent_blocks = []
|
||||
for name, agent in self.knowledge_microagents.items():
|
||||
trigger = agent.match_trigger(user_text)
|
||||
if trigger:
|
||||
logger.info("Microagent '%s' triggered by keyword '%s'", name, trigger)
|
||||
micro_text = (
|
||||
f'<extra_info>\n'
|
||||
f'The following information has been included based on a keyword match for "{trigger}". '
|
||||
f"It may or may not be relevant to the user's request.\n\n"
|
||||
f'{agent.content}\n'
|
||||
f'</extra_info>'
|
||||
)
|
||||
microagent_blocks.append(micro_text)
|
||||
|
||||
if microagent_blocks:
|
||||
# Combine all triggered microagents into a single RecallObservation
|
||||
combined_text = '\n'.join(microagent_blocks)
|
||||
obs = RecallObservation(content=combined_text)
|
||||
self.event_stream.add_event(
|
||||
obs, event.source if event.source else EventSource.ENVIRONMENT
|
||||
)
|
||||
|
||||
def _on_recall_action(self, event: RecallAction):
|
||||
"""If a RecallAction explicitly arrives, handle it."""
|
||||
assert isinstance(event, RecallAction)
|
||||
|
||||
user_query = event.query.get('keywords', [])
|
||||
matched_content = self.find_microagent_content(user_query)
|
||||
obs = RecallObservation(content=matched_content)
|
||||
self.event_stream.add_event(
|
||||
obs, event.source if event.source else EventSource.ENVIRONMENT
|
||||
)
|
||||
|
||||
def find_microagent_content(self, keywords: list[str]) -> str:
|
||||
"""Replicate the same microagent logic."""
|
||||
matched_texts: list[str] = []
|
||||
for name, agent in self.knowledge_microagents.items():
|
||||
for kw in keywords:
|
||||
trigger = agent.match_trigger(kw)
|
||||
if trigger:
|
||||
logger.info(
|
||||
"Microagent '%s' triggered by explicit RecallAction keyword '%s'",
|
||||
name,
|
||||
trigger,
|
||||
)
|
||||
block = (
|
||||
f'<extra_info>\n'
|
||||
f"(via RecallAction) Included knowledge from microagent '{name}', triggered by '{trigger}'\n\n"
|
||||
f'{agent.content}\n'
|
||||
f'</extra_info>'
|
||||
)
|
||||
matched_texts.append(block)
|
||||
return '\n'.join(matched_texts)
|
||||
|
||||
def load_user_workspace_microagents(
|
||||
self, user_microagents: list[BaseMicroAgent]
|
||||
) -> None:
|
||||
"""
|
||||
If you want to load microagents from a user's cloned repo or workspace directory,
|
||||
call this from agent_session or setup once the workspace is cloned.
|
||||
"""
|
||||
logger.info(
|
||||
'Loading user workspace microagents: %s', [m.name for m in user_microagents]
|
||||
)
|
||||
for ma in user_microagents:
|
||||
if ma.name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(ma, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[ma.name] = ma
|
||||
elif isinstance(ma, RepoMicroAgent):
|
||||
self.repo_microagents[ma.name] = ma
|
||||
|
||||
def set_prompt_manager(self, prompt_manager: PromptManager):
|
||||
self.prompt_manager = prompt_manager
|
||||
@@ -202,7 +202,7 @@ async def process_issue(
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
if os.getenv('GITLAB_CI') == 'true':
|
||||
if os.getenv('GITLAB_CI') == 'True':
|
||||
sandbox_config.local_runtime_url = os.getenv(
|
||||
'LOCAL_RUNTIME_URL', 'http://localhost'
|
||||
)
|
||||
@@ -651,7 +651,7 @@ def main() -> None:
|
||||
if not token:
|
||||
raise ValueError('Token is required.')
|
||||
|
||||
platform = identify_token(token, repo)
|
||||
platform = identify_token(token)
|
||||
if platform == Platform.INVALID:
|
||||
raise ValueError('Token is invalid.')
|
||||
|
||||
|
||||
@@ -22,37 +22,18 @@ class Platform(Enum):
|
||||
GITLAB = 2
|
||||
|
||||
|
||||
def identify_token(token: str, repo: str | None = None) -> Platform:
|
||||
def identify_token(token: str) -> Platform:
|
||||
"""
|
||||
Identifies whether a token belongs to GitHub or GitLab.
|
||||
|
||||
Parameters:
|
||||
token (str): The personal access token to check.
|
||||
repo (str): Repository in format "owner/repo" for GitHub Actions token validation.
|
||||
|
||||
Returns:
|
||||
Platform: "GitHub" if the token is valid for GitHub,
|
||||
"GitLab" if the token is valid for GitLab,
|
||||
"Invalid" if the token is not recognized by either.
|
||||
"""
|
||||
# Try GitHub Actions token format (Bearer) with repo endpoint if repo is provided
|
||||
if repo:
|
||||
github_repo_url = f'https://api.github.com/repos/{repo}'
|
||||
github_bearer_headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
}
|
||||
|
||||
try:
|
||||
github_repo_response = requests.get(
|
||||
github_repo_url, headers=github_bearer_headers, timeout=5
|
||||
)
|
||||
if github_repo_response.status_code == 200:
|
||||
return Platform.GITHUB
|
||||
except requests.RequestException as e:
|
||||
print(f'Error connecting to GitHub API (repo check): {e}')
|
||||
|
||||
# Try GitHub PAT format (token)
|
||||
github_url = 'https://api.github.com/user'
|
||||
github_headers = {'Authorization': f'token {token}'}
|
||||
|
||||
@@ -63,7 +44,6 @@ def identify_token(token: str, repo: str | None = None) -> Platform:
|
||||
except requests.RequestException as e:
|
||||
print(f'Error connecting to GitHub API: {e}')
|
||||
|
||||
# Try GitLab token
|
||||
gitlab_url = 'https://gitlab.com/api/v4/user'
|
||||
gitlab_headers = {'Authorization': f'Bearer {token}'}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from fastapi.security import APIKeyHeader
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
from openhands_aci.editor.exceptions import ToolError
|
||||
from openhands_aci.editor.results import ToolResult
|
||||
from openhands_aci.utils.diff import get_diff
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
@@ -90,7 +89,7 @@ def _execute_file_editor(
|
||||
new_str: str | None = None,
|
||||
insert_line: int | None = None,
|
||||
enable_linting: bool = False,
|
||||
) -> tuple[str, tuple[str | None, str | None]]:
|
||||
) -> str:
|
||||
"""Execute file editor command and handle exceptions.
|
||||
|
||||
Args:
|
||||
@@ -105,7 +104,7 @@ def _execute_file_editor(
|
||||
enable_linting: Whether to enable linting
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the output string and a tuple of old and new file content
|
||||
str: Result string from the editor operation
|
||||
"""
|
||||
result: ToolResult | None = None
|
||||
try:
|
||||
@@ -123,13 +122,13 @@ def _execute_file_editor(
|
||||
result = ToolResult(error=e.message)
|
||||
|
||||
if result.error:
|
||||
return f'ERROR:\n{result.error}', (None, None)
|
||||
return f'ERROR:\n{result.error}'
|
||||
|
||||
if not result.output:
|
||||
logger.warning(f'No output from file_editor for {path}')
|
||||
return '', (None, None)
|
||||
return ''
|
||||
|
||||
return result.output, (result.old_content, result.new_content)
|
||||
return result.output
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
@@ -317,7 +316,7 @@ class ActionExecutor:
|
||||
async def read(self, action: FileReadAction) -> Observation:
|
||||
assert self.bash_session is not None
|
||||
if action.impl_source == FileReadSource.OH_ACI:
|
||||
result_str, _ = _execute_file_editor(
|
||||
result_str = _execute_file_editor(
|
||||
self.file_editor,
|
||||
command='view',
|
||||
path=action.path,
|
||||
@@ -434,7 +433,7 @@ class ActionExecutor:
|
||||
|
||||
async def edit(self, action: FileEditAction) -> Observation:
|
||||
assert action.impl_source == FileEditSource.OH_ACI
|
||||
result_str, (old_content, new_content) = _execute_file_editor(
|
||||
result_str = _execute_file_editor(
|
||||
self.file_editor,
|
||||
command=action.command,
|
||||
path=action.path,
|
||||
@@ -451,11 +450,6 @@ class ActionExecutor:
|
||||
old_content=action.old_str,
|
||||
new_content=action.new_str,
|
||||
impl_source=FileEditSource.OH_ACI,
|
||||
diff=get_diff(
|
||||
old_contents=old_content or '',
|
||||
new_contents=new_content or '',
|
||||
filepath=action.path,
|
||||
),
|
||||
)
|
||||
|
||||
async def browse(self, action: BrowseURLAction) -> Observation:
|
||||
|
||||
@@ -222,7 +222,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
if isinstance(event, CmdRunAction):
|
||||
if self.github_user_id and '$GITHUB_TOKEN' in event.command:
|
||||
gh_client = GithubServiceImpl(user_id=self.github_user_id)
|
||||
token = await gh_client.get_latest_provider_token()
|
||||
token = await gh_client.get_latest_token()
|
||||
if token:
|
||||
export_cmd = CmdRunAction(
|
||||
f"export GITHUB_TOKEN='{token.get_secret_value()}'"
|
||||
|
||||
@@ -19,30 +19,19 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
|
||||
version_info = self.docker_client.version()
|
||||
server_version = version_info.get('Version', '').replace('-', '.')
|
||||
self.is_podman = (
|
||||
version_info.get('Components')[0].get('Name').startswith('Podman')
|
||||
)
|
||||
if (
|
||||
tuple(map(int, server_version.split('.')[:2])) < (18, 9)
|
||||
and not self.is_podman
|
||||
):
|
||||
if tuple(map(int, server_version.split('.')[:2])) < (18, 9):
|
||||
raise AgentRuntimeBuildError(
|
||||
'Docker server version must be >= 18.09 to use BuildKit'
|
||||
)
|
||||
|
||||
if self.is_podman and tuple(map(int, server_version.split('.')[:2])) < (4, 9):
|
||||
raise AgentRuntimeBuildError('Podman server version must be >= 4.9.0')
|
||||
|
||||
self.rolling_logger = RollingLogger(max_lines=10)
|
||||
|
||||
@staticmethod
|
||||
def check_buildx(is_podman: bool = False):
|
||||
def check_buildx():
|
||||
"""Check if Docker Buildx is available"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker' if not is_podman else 'podman', 'buildx', 'version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
['docker', 'buildx', 'version'], capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
except FileNotFoundError:
|
||||
@@ -79,18 +68,12 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
self.docker_client = docker.from_env()
|
||||
version_info = self.docker_client.version()
|
||||
server_version = version_info.get('Version', '').split('+')[0].replace('-', '.')
|
||||
self.is_podman = (
|
||||
version_info.get('Components')[0].get('Name').startswith('Podman')
|
||||
)
|
||||
if tuple(map(int, server_version.split('.'))) < (18, 9) and not self.is_podman:
|
||||
if tuple(map(int, server_version.split('.'))) < (18, 9):
|
||||
raise AgentRuntimeBuildError(
|
||||
'Docker server version must be >= 18.09 to use BuildKit'
|
||||
)
|
||||
|
||||
if self.is_podman and tuple(map(int, server_version.split('.'))) < (4, 9):
|
||||
raise AgentRuntimeBuildError('Podman server version must be >= 4.9.0')
|
||||
|
||||
if not DockerRuntimeBuilder.check_buildx(self.is_podman):
|
||||
if not DockerRuntimeBuilder.check_buildx():
|
||||
# when running openhands in a container, there might not be a "docker"
|
||||
# binary available, in which case we need to download docker binary.
|
||||
# since the official openhands app image is built from debian, we use
|
||||
@@ -127,7 +110,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None
|
||||
|
||||
buildx_cmd = [
|
||||
'docker' if not self.is_podman else 'podman',
|
||||
'docker',
|
||||
'buildx',
|
||||
'build',
|
||||
'--progress=plain',
|
||||
@@ -156,7 +139,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
|
||||
buildx_cmd.append(path) # must be last!
|
||||
|
||||
self.rolling_logger.start(
|
||||
f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
|
||||
'================ DOCKER BUILD STARTED ================'
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,80 +2,22 @@
|
||||
|
||||
[Daytona](https://www.daytona.io/) is a platform that provides a secure and elastic infrastructure for running AI-generated code. It provides all the necessary features for an AI Agent to interact with a codebase. It provides a Daytona SDK with official Python and TypeScript interfaces for interacting with Daytona, enabling you to programmatically manage development environments and execute code.
|
||||
|
||||
## Quick Start
|
||||
## Getting started
|
||||
|
||||
### 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.
|
||||
1. Sign in at https://app.daytona.io/
|
||||
|
||||
1. Generate and copy your API key
|
||||
|
||||
1. Set the following environment variables before running the OpenHands app on your local machine or via a `docker run` command:
|
||||
|
||||
### 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>"
|
||||
RUNTIME="daytona"
|
||||
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.
|
||||
|
||||
|
||||
## Manual Initialization
|
||||
|
||||
### Step 1: Set the `OPENHANDS_VERSION` Environment Variable
|
||||
Run the following command in your terminal, replacing `<openhands-release>` with the latest release's version seen in the [main README.md file](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-quick-start):
|
||||
Optionally, if you don't want your sandboxes to default to the US region, set:
|
||||
|
||||
```bash
|
||||
export OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
|
||||
```
|
||||
|
||||
### Step 2: 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 3: 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>"
|
||||
```
|
||||
|
||||
### Step 4: Run the following `docker` command:
|
||||
This command pulls and runs the OpenHands container using Docker. Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
```bash
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${OPENHANDS_VERSION}-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-e RUNTIME=daytona \
|
||||
-e DAYTONA_API_KEY=${DAYTONA_API_KEY} \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:${OPENHANDS_VERSION}
|
||||
```
|
||||
|
||||
> **Tip:** If you don't want your sandboxes to default to the US region, you can set the `DAYTONA_TARGET` environment variable to `eu`
|
||||
|
||||
### Running OpenHands Locally Without Docker
|
||||
|
||||
Alternatively, if you want to run the OpenHands app on your local machine using `make run` without Docker, make sure to set the following environment variables first:
|
||||
|
||||
```bash
|
||||
export RUNTIME="daytona"
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
DAYTONA_TARGET="eu"
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -201,29 +201,16 @@ class DockerRuntime(ActionExecutionClient):
|
||||
port_mapping: dict[str, list[dict[str, str]]] | None = None
|
||||
if not use_host_network:
|
||||
port_mapping = {
|
||||
f'{self._container_port}/tcp': [
|
||||
{
|
||||
'HostPort': str(self._host_port),
|
||||
'HostIp': self.config.sandbox.runtime_binding_address,
|
||||
}
|
||||
],
|
||||
f'{self._container_port}/tcp': [{'HostPort': str(self._host_port)}],
|
||||
}
|
||||
|
||||
if self.vscode_enabled:
|
||||
port_mapping[f'{self._vscode_port}/tcp'] = [
|
||||
{
|
||||
'HostPort': str(self._vscode_port),
|
||||
'HostIp': self.config.sandbox.runtime_binding_address,
|
||||
}
|
||||
{'HostPort': str(self._vscode_port)}
|
||||
]
|
||||
|
||||
for port in self._app_ports:
|
||||
port_mapping[f'{port}/tcp'] = [
|
||||
{
|
||||
'HostPort': str(port),
|
||||
'HostIp': self.config.sandbox.runtime_binding_address,
|
||||
}
|
||||
]
|
||||
port_mapping[f'{port}/tcp'] = [{'HostPort': str(port)}]
|
||||
else:
|
||||
self.log(
|
||||
'warn',
|
||||
@@ -319,7 +306,6 @@ class DockerRuntime(ActionExecutionClient):
|
||||
self.container = self.docker_client.containers.get(self.container_name)
|
||||
if self.container.status == 'exited':
|
||||
self.container.start()
|
||||
|
||||
config = self.container.attrs['Config']
|
||||
for env_var in config['Env']:
|
||||
if env_var.startswith('port='):
|
||||
@@ -327,18 +313,11 @@ class DockerRuntime(ActionExecutionClient):
|
||||
self._container_port = self._host_port
|
||||
elif env_var.startswith('VSCODE_PORT='):
|
||||
self._vscode_port = int(env_var.split('VSCODE_PORT=')[1])
|
||||
|
||||
self._app_ports = []
|
||||
exposed_ports = config.get('ExposedPorts')
|
||||
if exposed_ports:
|
||||
for exposed_port in exposed_ports.keys():
|
||||
exposed_port = int(exposed_port.split('/tcp')[0])
|
||||
if (
|
||||
exposed_port != self._host_port
|
||||
and exposed_port != self._vscode_port
|
||||
):
|
||||
self._app_ports.append(exposed_port)
|
||||
|
||||
for exposed_port in config['ExposedPorts'].keys():
|
||||
exposed_port = int(exposed_port.split('/tcp')[0])
|
||||
if exposed_port != self._host_port and exposed_port != self._vscode_port:
|
||||
self._app_ports.append(exposed_port)
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
self.log(
|
||||
'debug',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.action import (
|
||||
@@ -27,7 +27,7 @@ class E2BRuntime(Runtime):
|
||||
sid: str = 'default',
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
sandbox: E2BSandbox | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
status_callback: Optional[Callable] = None,
|
||||
):
|
||||
super().__init__(
|
||||
config,
|
||||
|
||||
@@ -7,7 +7,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
import requests
|
||||
import tenacity
|
||||
@@ -155,7 +155,7 @@ class LocalRuntime(ActionExecutionClient):
|
||||
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._host_port}'
|
||||
self.status_callback = status_callback
|
||||
self.server_process: subprocess.Popen[str] | None = None
|
||||
self.server_process: Optional[subprocess.Popen[str]] = None
|
||||
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
|
||||
|
||||
# Update env vars
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
@@ -42,7 +42,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
sid: str = 'default',
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
status_callback: Optional[Callable] = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
github_user_id: str | None = None,
|
||||
|
||||
@@ -45,4 +45,4 @@ This extension is part of the OpenHands project. To modify or extend it:
|
||||
|
||||
## License
|
||||
|
||||
This extension is licensed under the MIT license.
|
||||
This extension is licensed under the MIT license.
|
||||
@@ -4,30 +4,30 @@ const MemoryMonitor = require('./memory_monitor');
|
||||
function activate(context) {
|
||||
// Create memory monitor instance
|
||||
const memoryMonitor = new MemoryMonitor();
|
||||
|
||||
|
||||
// Store the context in the memory monitor
|
||||
memoryMonitor.context = context;
|
||||
|
||||
|
||||
// Register memory monitor start command
|
||||
let startMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.startMemoryMonitor', function () {
|
||||
memoryMonitor.start();
|
||||
});
|
||||
|
||||
|
||||
// Register memory monitor stop command
|
||||
let stopMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.stopMemoryMonitor', function () {
|
||||
memoryMonitor.stop();
|
||||
});
|
||||
|
||||
|
||||
// Register memory details command
|
||||
let showMemoryDetailsCommand = vscode.commands.registerCommand('openhands-memory-monitor.showMemoryDetails', function () {
|
||||
memoryMonitor.showDetails();
|
||||
});
|
||||
|
||||
|
||||
// Add all commands to subscriptions
|
||||
context.subscriptions.push(startMonitorCommand);
|
||||
context.subscriptions.push(stopMonitorCommand);
|
||||
context.subscriptions.push(showMemoryDetailsCommand);
|
||||
|
||||
|
||||
// Start memory monitoring by default
|
||||
memoryMonitor.start();
|
||||
}
|
||||
@@ -39,4 +39,4 @@ function deactivate() {
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,15 @@ class MemoryMonitor {
|
||||
|
||||
this.isMonitoring = true;
|
||||
this.statusBarItem.show();
|
||||
|
||||
|
||||
// Initial update
|
||||
this.updateMemoryInfo();
|
||||
|
||||
|
||||
// Set interval for updates
|
||||
this.intervalId = setInterval(() => {
|
||||
this.updateMemoryInfo();
|
||||
}, interval);
|
||||
|
||||
|
||||
vscode.window.showInformationMessage('Memory monitoring started');
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class MemoryMonitor {
|
||||
this.isMonitoring = false;
|
||||
clearInterval(this.intervalId);
|
||||
this.statusBarItem.hide();
|
||||
|
||||
|
||||
vscode.window.showInformationMessage('Memory monitoring stopped');
|
||||
}
|
||||
|
||||
@@ -49,18 +49,18 @@ class MemoryMonitor {
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
|
||||
|
||||
// Calculate memory usage percentage
|
||||
const memUsagePercent = Math.round((usedMem / totalMem) * 100);
|
||||
|
||||
|
||||
// Format memory values to MB
|
||||
const usedMemMB = Math.round(usedMem / (1024 * 1024));
|
||||
const totalMemMB = Math.round(totalMem / (1024 * 1024));
|
||||
|
||||
|
||||
// Update status bar
|
||||
this.statusBarItem.text = `$(pulse) Mem: ${memUsagePercent}%`;
|
||||
this.statusBarItem.tooltip = `Memory Usage: ${usedMemMB}MB / ${totalMemMB}MB`;
|
||||
|
||||
|
||||
// Store memory data in history
|
||||
this.memoryHistory.push({
|
||||
timestamp: new Date(),
|
||||
@@ -69,7 +69,7 @@ class MemoryMonitor {
|
||||
memUsagePercent,
|
||||
processMemory: process.memoryUsage()
|
||||
});
|
||||
|
||||
|
||||
// Limit history length
|
||||
if (this.memoryHistory.length > this.maxHistoryLength) {
|
||||
this.memoryHistory.shift();
|
||||
@@ -86,7 +86,7 @@ class MemoryMonitor {
|
||||
enableScripts: true
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Set up message handler for real-time updates
|
||||
panel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
@@ -97,60 +97,60 @@ class MemoryMonitor {
|
||||
undefined,
|
||||
this.context ? this.context.subscriptions : []
|
||||
);
|
||||
|
||||
|
||||
// Initial update
|
||||
this.updateWebviewContent(panel);
|
||||
|
||||
|
||||
// Handle panel disposal
|
||||
panel.onDidDispose(() => {
|
||||
// Clean up any resources if needed
|
||||
}, null, this.context ? this.context.subscriptions : []);
|
||||
}
|
||||
|
||||
|
||||
updateWebviewContent(panel) {
|
||||
// Get system memory info
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
|
||||
|
||||
// Format memory values
|
||||
const usedMemMB = Math.round(usedMem / (1024 * 1024));
|
||||
const freeMemMB = Math.round(freeMem / (1024 * 1024));
|
||||
const totalMemMB = Math.round(totalMem / (1024 * 1024));
|
||||
|
||||
|
||||
// Get process memory usage
|
||||
const processMemory = process.memoryUsage();
|
||||
const rss = Math.round(processMemory.rss / (1024 * 1024));
|
||||
const heapTotal = Math.round(processMemory.heapTotal / (1024 * 1024));
|
||||
const heapUsed = Math.round(processMemory.heapUsed / (1024 * 1024));
|
||||
|
||||
|
||||
// Get process information
|
||||
this.processMonitor.getProcessInfo((error, processInfo) => {
|
||||
if (error) {
|
||||
console.error('Error getting process info:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create HTML content for the webview
|
||||
const htmlContent = this.generateHtmlReport(
|
||||
usedMemMB, freeMemMB, totalMemMB,
|
||||
usedMemMB, freeMemMB, totalMemMB,
|
||||
rss, heapTotal, heapUsed,
|
||||
processInfo
|
||||
);
|
||||
|
||||
|
||||
// Set the webview's HTML content
|
||||
panel.webview.html = htmlContent;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateHtmlReport(usedMemMB, freeMemMB, totalMemMB, rss, heapTotal, heapUsed, processInfo) {
|
||||
// Create memory usage history data for chart
|
||||
const memoryLabels = this.memoryHistory.map((entry, index) => index);
|
||||
const memoryData = this.memoryHistory.map(entry => entry.memUsagePercent);
|
||||
const heapData = this.memoryHistory.map(entry =>
|
||||
const heapData = this.memoryHistory.map(entry =>
|
||||
Math.round(entry.processMemory.heapUsed / (1024 * 1024))
|
||||
);
|
||||
|
||||
|
||||
// Format process info table
|
||||
let processTable = '';
|
||||
if (processInfo && processInfo.processes) {
|
||||
@@ -174,7 +174,7 @@ class MemoryMonitor {
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -237,7 +237,7 @@ class MemoryMonitor {
|
||||
</head>
|
||||
<body>
|
||||
<h1>Memory Monitor</h1>
|
||||
|
||||
|
||||
<div class="memory-card">
|
||||
<h2>System Memory</h2>
|
||||
<div class="memory-info">
|
||||
@@ -259,7 +259,7 @@ class MemoryMonitor {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="memory-card">
|
||||
<h2>Process Memory (VSCode Extension Host)</h2>
|
||||
<div class="memory-info">
|
||||
@@ -277,18 +277,18 @@ class MemoryMonitor {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="memory-card">
|
||||
<h2>Memory Usage History</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="memoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="memory-card">
|
||||
${processTable}
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Create memory usage chart
|
||||
const ctx = document.getElementById('memoryChart').getContext('2d');
|
||||
@@ -323,10 +323,10 @@ class MemoryMonitor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Set up real-time updates
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
|
||||
// Request updates every 5 seconds
|
||||
setInterval(() => {
|
||||
vscode.postMessage({
|
||||
@@ -340,4 +340,4 @@ class MemoryMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MemoryMonitor;
|
||||
module.exports = MemoryMonitor;
|
||||
@@ -47,4 +47,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user