Compare commits

..

35 Commits

Author SHA1 Message Date
openhands 489e32c2c0 Fix email update to use /api/settings endpoint 2025-05-31 19:10:03 +00:00
openhands c189012f0a Fix email update to use query parameter instead of form data 2025-05-31 19:05:02 +00:00
openhands 2407420e17 Make email field editable and add save button in user settings 2025-05-31 18:59:14 +00:00
chuckbutkus bb0c47c41a Merge branch 'main' into display-email 2025-05-31 01:14:24 -04:00
Chuck Butkus 83e5276de5 Update User Setting tab 2025-05-31 01:13:38 -04:00
openhands 816082a55b Update User tab to display email from settings instead of git user 2025-05-31 04:12:04 +00:00
Chuck Butkus 82d72b145d Add email to Setting class 2025-05-30 23:59:47 -04:00
Xingyao Wang 277b87413b chore: log stack trace when failed to connect in mcp (#8804) 2025-05-30 18:35:19 -04:00
Rohit Malhotra 972837ff73 [Feat]: Gitlab resolver (#8770) 2025-05-30 22:18:34 +00:00
Chuck Butkus f8c3470c91 Add get_user_email from UserAuth 2025-05-30 15:43:08 -04:00
tofarr de196d6da1 Allowing local runtimes to have domains (#8798) 2025-05-29 15:15:27 -06:00
Rohit Malhotra 10a4ab9199 [Fix]: inconsistent microagent descriptions (#8800) 2025-05-29 20:09:01 +00:00
Robert Brennan 3f0d8e7f47 add more logging to debug runtime restarts (#8799) 2025-05-29 19:52:13 +00:00
Engel Nyst 0e0687efc3 Rename service (#8791) 2025-05-29 19:43:42 +00:00
tofarr 30d53f8df0 Fix openapi authorize (#8794) 2025-05-29 13:20:00 -06:00
sp.wack fd52841842 Add git_provider and selected_branch to conversation response (#8795)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-29 22:59:30 +04:00
Graham Neubig d770edc010 Add images and example commands to cloud issue resolver documentation (#8785)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-29 18:13:49 +00:00
Engel Nyst df2374bd85 Refactor agent controller's filter (#8792) 2025-05-29 19:59:34 +02:00
sp.wack 542be7338e fix(frontend): Replace actions with their observations pairs in the chat interface (#8760)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-05-29 19:22:10 +02:00
Rohit Malhotra e4d84946db (Chore): deprecate alias (#8704) 2025-05-29 11:01:28 -04:00
tofarr cc881a6bcb Added Webhooks to event store (#8763)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-29 07:39:08 -06:00
Howie Zhou cb0a1c91e4 docs: change to existing testing files (#8779) 2025-05-29 12:23:24 +00:00
Graham Neubig e208bffade Fix localization issue with hardcoded English strings (#8736)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-29 00:40:57 +02:00
Graham Neubig 6491142364 Fix KeyError on router error logging (#8769) 2025-05-28 19:59:18 +00:00
Robert Brennan 205f0234e8 Rename Conversation to ServerConversation and AppConfig to OpenHandsConfig (#8754)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 21:48:34 +02:00
Robert Brennan c76809a766 Revert "Add username parameter to AsyncBashSession" (#8767) 2025-05-28 14:28:26 -04:00
chuckbutkus 9f86f731a7 Update login (#8743)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 17:53:35 +00:00
sp.wack 6fe5da810b fix(frontend): Handle assistant messages at the top (#8766) 2025-05-28 17:33:05 +00:00
dependabot[bot] 52a1e94335 chore(deps): bump the docusaurus group in /docs with 7 updates (#8758)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 21:24:54 +04:00
sp.wack 3e0532e8b9 fix(frontend): Only clear UI messages on cid change (#8762) 2025-05-28 15:31:34 +00:00
tofarr 90c440d709 Add HTTP FileStore implementation (#8751)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 08:17:26 -06:00
Robert Brennan 82657b7ba1 Add username parameter to AsyncBashSession (#8746)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 10:16:45 -04:00
Engel Nyst 3c51600260 Add vscode rules/ignores to .gitignore (#8755) 2025-05-28 15:42:11 +02:00
sp.wack b5f2a04ea2 Add refill link to out-of-credits error message (#8737)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 17:23:14 +04:00
sp.wack 155615bbb1 hotfix(frontend): Invalidate and refetch git changes if messages aren't being received (#8752) 2025-05-28 13:22:15 +00:00
209 changed files with 3564 additions and 3174 deletions
+14 -1
View File
@@ -161,7 +161,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.cursorignore
# VS Code: Ignore all but certain files that specify repo-specific settings.
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
@@ -171,6 +170,20 @@ cython_debug/
!.vscode/settings.json
!.vscode/tasks.json
# VS Code extensions/forks:
.cursorignore
.rooignore
.clineignore
.windsurfignore
.cursorrules
.roorules
.clinerules
.windsurfrules
.cursor/rules
.roo/rules
.cline/rules
.windsurf/rules
# evaluation
evaluation/evaluation_outputs
evaluation/outputs
-19
View File
@@ -1,19 +0,0 @@
[ ] Rename `Conversation` in openhands/server to `ServerConversation`
[ ] Replace all instances of `sid` in openhands/* to `conversation_id`
[ ] Make EventStream take in a `conversation_id` in its constructor.
* remove `conversation_id` from all methods on EventStream and use self.conversation_id instead.
* fix all callers of EventStream to pass in `conversation_id` in the constructor and remove it from the method calls.
[ ] Rename AppConfig to OpenHandsConfig
[ ] Create a new class `Conversation` in openhands/core/ that will be the main interface for conversations.
* Its constructor will take in a:
* conversation_id (string)
* Runtime
* LLM
* EventStream
* AgentController
* No logic, it's just a dataclass
[ ] Add a new OpenHands class to openhands/core/ which will take care of creating Conversations
* Constructor is ONLY an OpenHandsConfig
* Only one method: `create_conversation()`
* This will create a Runtime, LLM, EventStream, and AgentController, and return a Conversation object.
* These objects will be created according to the OpenHandsConfig passed in to the constructor.
@@ -8,18 +8,30 @@ Le Résolveur de Problèmes Cloud est disponible automatiquement lorsque vous ac
- [Accès au dépôt GitHub](./github-installation#adding-repository-access)
- [Accès au dépôt GitLab](./gitlab-installation#adding-repository-access)
![Ajout d'accès au dépôt à OpenHands](/img/cloud/add-repo.png)
## Utilisation
Après avoir accordé l'accès au dépôt OpenHands Cloud, vous pouvez utiliser le Résolveur de Problèmes Cloud sur les problèmes et les pull/merge requests dans vos dépôts.
### Travailler avec les Problèmes
Sur votre dépôt, étiquetez un problème avec `openhands`. OpenHands va :
Sur votre dépôt, étiquetez un problème avec `openhands` ou ajoutez un message commençant par `@openhands`. OpenHands va :
1. Commenter le problème pour vous faire savoir qu'il y travaille
- Vous pouvez cliquer sur le lien pour suivre la progression sur OpenHands Cloud
2. Ouvrir une pull request (GitHub) ou une merge request (GitLab) s'il détermine que le problème a été résolu avec succès
3. Commenter le problème avec un résumé des tâches effectuées et un lien vers la PR/MR
![Résolveur de problèmes OpenHands en action](/img/cloud/issue-resolver.png)
#### Exemples de Commandes pour les Problèmes
Voici quelques exemples de commandes que vous pouvez utiliser avec le résolveur de problèmes :
```
@openhands lisez la description du problème et corrigez-le
```
### Travailler avec les Pull/Merge Requests
Pour qu'OpenHands travaille sur les pull requests (GitHub) ou les merge requests (GitLab), mentionnez `@openhands` dans les commentaires pour :
@@ -30,3 +42,15 @@ Pour qu'OpenHands travaille sur les pull requests (GitHub) ou les merge requests
OpenHands va :
1. Commenter pour vous faire savoir qu'il y travaille
2. Effectuer la tâche demandée
#### Exemples de Commandes pour les Pull/Merge Requests
Voici quelques exemples de commandes que vous pouvez utiliser avec les pull/merge requests :
```
@openhands reflétez les commentaires de la revue
```
```
@openhands corrigez les conflits de fusion et assurez-vous que le CI passe
```
@@ -36,6 +36,8 @@ Vous pouvez accorder à OpenHands l'accès à des dépôts spécifiques :
- Permissions GitHub de l'utilisateur (propriétaire/collaborateur)
3. Cliquez sur `Installer & Autoriser`
![Ajout de l'accès au dépôt à OpenHands](/img/cloud/add-repo.png)
## Modification de l'Accès au Dépôt
Vous pouvez modifier l'accès au dépôt à tout moment :
@@ -8,18 +8,30 @@
- [GitHubリポジトリアクセス](./github-installation#adding-repository-access)
- [GitLabリポジトリアクセス](./gitlab-installation#adding-repository-access)
![OpenHandsにリポジトリアクセスを追加する](/img/cloud/add-repo.png)
## 使用方法
OpenHands Cloudリポジトリアクセスを許可した後、リポジトリの課題やプルリクエスト/マージリクエストでクラウド課題リゾルバーを使用できます。
### 課題の操作
リポジトリで、課題に`openhands`というラベルを付けます。OpenHandsは以下を行います:
リポジトリで、課題に`openhands`というラベルを付けるか、`@openhands`で始まるメッセージを追加します。OpenHandsは以下を行います:
1. 課題にコメントして、作業中であることを知らせます
- リンクをクリックすると、OpenHands Cloudで進捗状況を追跡できます
2. 課題が正常に解決されたと判断した場合、プルリクエスト(GitHub)またはマージリクエスト(GitLab)を開きます
3. 実行されたタスクの概要とPR/MRへのリンクを含むコメントを課題に残します
![OpenHands課題リゾルバーの動作](/img/cloud/issue-resolver.png)
#### 課題用のコマンド例
以下は、課題リゾルバーで使用できるコマンドの例です:
```
@openhands 課題の説明を読んで修正してください
```
### プルリクエスト/マージリクエストの操作
プルリクエスト(GitHub)またはマージリクエスト(GitLab)でOpenHandsを動作させるには、コメントで`@openhands`を言及して以下を行います:
@@ -30,3 +42,15 @@ OpenHands Cloudリポジトリアクセスを許可した後、リポジトリ
OpenHandsは以下を行います:
1. 作業中であることを知らせるコメントをします
2. リクエストされたタスクを実行します
#### プルリクエスト/マージリクエスト用のコマンド例
以下は、プルリクエスト/マージリクエストで使用できるコマンドの例です:
```
@openhands レビューコメントを反映してください
```
```
@openhands マージの競合を修正し、CIが通過することを確認してください
```
@@ -36,6 +36,8 @@
- ユーザーのGitHub権限(所有者/コラボレーター)
3. `インストール&承認`をクリックします
![OpenHandsへのリポジトリアクセスの追加](/img/cloud/add-repo.png)
## リポジトリアクセスの変更
リポジトリアクセスはいつでも変更できます:
@@ -8,18 +8,30 @@ O Resolvedor de Problemas na Nuvem está disponível automaticamente quando voc
- [Acesso ao repositório GitHub](./github-installation#adding-repository-access)
- [Acesso ao repositório GitLab](./gitlab-installation#adding-repository-access)
![Adicionando acesso ao repositório ao OpenHands](/img/cloud/add-repo.png)
## Uso
Após conceder acesso ao repositório OpenHands Cloud, você pode usar o Resolvedor de Problemas na Nuvem em issues e pull/merge requests em seus repositórios.
### Trabalhando com Issues
No seu repositório, rotule uma issue com `openhands`. O OpenHands irá:
No seu repositório, rotule uma issue com `openhands` ou adicione uma mensagem começando com `@openhands`. O OpenHands irá:
1. Comentar na issue para informar que está trabalhando nela
- Você pode clicar no link para acompanhar o progresso no OpenHands Cloud
2. Abrir um pull request (GitHub) ou merge request (GitLab) se determinar que o problema foi resolvido com sucesso
3. Comentar na issue com um resumo das tarefas realizadas e um link para o PR/MR
![OpenHands resolvedor de problemas em ação](/img/cloud/issue-resolver.png)
#### Exemplos de Comandos para Issues
Aqui estão alguns exemplos de comandos que você pode usar com o resolvedor de problemas:
```
@openhands leia a descrição do problema e corrija-o
```
### Trabalhando com Pull/Merge Requests
Para fazer o OpenHands trabalhar em pull requests (GitHub) ou merge requests (GitLab), mencione `@openhands` nos comentários para:
@@ -30,3 +42,15 @@ Para fazer o OpenHands trabalhar em pull requests (GitHub) ou merge requests (Gi
O OpenHands irá:
1. Comentar para informar que está trabalhando nisso
2. Realizar a tarefa solicitada
#### Exemplos de Comandos para Pull/Merge Requests
Aqui estão alguns exemplos de comandos que você pode usar com pull/merge requests:
```
@openhands reflita os comentários da revisão
```
```
@openhands corrija os conflitos de merge e certifique-se de que o CI passa
```
@@ -36,6 +36,8 @@ Você pode conceder ao OpenHands acesso a repositórios específicos:
- Permissões do GitHub do usuário (proprietário/colaborador)
3. Clique em `Instalar e Autorizar`
![Adicionando acesso ao repositório ao OpenHands](/img/cloud/add-repo.png)
## Modificando o Acesso ao Repositório
Você pode modificar o acesso ao repositório a qualquer momento:
@@ -8,18 +8,30 @@
- [GitHub仓库访问](./github-installation#adding-repository-access)
- [GitLab仓库访问](./gitlab-installation#adding-repository-access)
![向OpenHands添加仓库访问权限](/img/cloud/add-repo.png)
## 使用方法
授予OpenHands Cloud仓库访问权限后,您可以在仓库的问题和拉取/合并请求中使用云问题解决器。
### 处理问题
在您的仓库中,给问题添加`openhands`标签。OpenHands将会:
在您的仓库中,给问题添加`openhands`标签或添加以`@openhands`开头的消息。OpenHands将会:
1. 在问题上发表评论,让您知道它正在处理
- 您可以点击链接在OpenHands Cloud上跟踪进度
2. 如果确定问题已成功解决,则打开拉取请求(GitHub)或合并请求(GitLab
3. 在问题上发表评论,总结已执行的任务并提供PR/MR的链接
![OpenHands问题解决器运行中](/img/cloud/issue-resolver.png)
#### 问题命令示例
以下是您可以在问题解决器中使用的命令示例:
```
@openhands 阅读问题描述并修复它
```
### 处理拉取/合并请求
要让OpenHands处理拉取请求(GitHub)或合并请求(GitLab),在评论中提及`@openhands`以:
@@ -30,3 +42,15 @@
OpenHands将会:
1. 发表评论让您知道它正在处理
2. 执行请求的任务
#### 拉取/合并请求命令示例
以下是您可以在拉取/合并请求中使用的命令示例:
```
@openhands 反映审查评论
```
```
@openhands 修复合并冲突并确保CI通过
```
@@ -36,6 +36,8 @@
- 用户的GitHub权限(所有者/协作者)
3. 点击`安装并授权`
![向OpenHands添加仓库访问权限](/img/cloud/add-repo.png)
## 修改仓库访问权限
您可以随时修改仓库访问权限:
-84
View File
@@ -1,84 +0,0 @@
# Using OpenHands as a library
## Hello World
```python
import asyncio
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
from openhands.core.setup import run_agent
async def run_openhands_agent():
final_state = await run_agent(
config=OpenHandsConfig(
llm=LLMConfig(
model="claude-sonnet-4-20250514",
api_key="your_api_key_here", # Replace with your actual API key
),
),
initial_user_message="Flip a coin",
context_message="You build simple programs and run them.",
)
return final_state
# Run the async function
if __name__ == "__main__":
final_state = asyncio.run(run_openhands_agent())
print("Agent execution completed!")
```
## Using the internals
```python
import asyncio
from openhands.controller.agent import Agent
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
from openhands.events.action import MessageAction
from openhands.llm.llm import LLM
from openhands.core.setup import (
create_runtime,
create_memory,
generate_sid,
)
from openhands.core.main import run_controller
async def run_openhands_agent():
config = OpenHandsConfig(
runtime="local",
file_store="memory",
llm=LLMConfig(
model="claude-sonnet-4-20250514", # Choose your preferred model
api_key="your_api_key_here", # Replace with your actual API key
temperature=0.0, # Set temperature to 0 for deterministic output
),
agent=AgentConfig(
enable_browsing=False,
),
)
oh = OpenHands(config=config)
conversation = oh.create_conversation(
conversation_id='hello-world',
)
await conversation.runtime.connect()
def on_event(event: Event) -> None:
print(f"Event received: {event}")
conversation.event_stream.subscribe(EventStreamSubscriber.MAIN, on_event)
initial_user_action = MessageAction(content="Flip a coin")
conversation.event_stream.add_event(initial_user_action, EventSource.USER)
while conversation.state.agent_state not in end_states:
await asyncio.sleep(1)
await runtime.close()
return conversation.state
# Run the async function
if __name__ == "__main__":
final_state = asyncio.run(run_openhands_agent())
print("Agent execution completed!")
```
+1 -1
View File
@@ -1,5 +1,5 @@
{
"items": ["python/python", "python/using-openhands-as-library"],
"items": ["python/python"],
"label": "Backend",
"type": "category"
}
@@ -1,399 +0,0 @@
# Using OpenHands as a Library
OpenHands can be used as a Python library in your own applications. This guide will show you how to integrate OpenHands into your Python projects, allowing you to build custom applications that leverage OpenHands' powerful agent capabilities.
## Installation
First, install the OpenHands library from PyPI:
```bash
pip install openhands-ai
```
## Basic Usage
Here's a simple example of how to use OpenHands in your Python code:
```python
import asyncio
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig, LLMConfig, AgentConfig
from openhands.events.action import MessageAction
from openhands.llm.llm import LLM
from openhands.core.setup import (
create_runtime,
create_memory,
generate_sid,
)
from openhands.core.main import run_controller
async def run_openhands_agent():
# 1. Create configuration
config = AppConfig(
runtime="local", # Use local runtime
file_store="memory", # Store events in memory
)
# 2. Configure LLM
llm_config = LLMConfig(
model="claude-sonnet-4-20250514", # Choose your preferred model
api_key="your_api_key_here", # Replace with your actual API key
temperature=0.0,
)
config.set_llm_config(llm_config)
# 3. Configure Agent
agent_config = AgentConfig(
enable_browsing=False, # Disable browsing for this example
)
config.set_agent_config(agent_config)
# 4. Create Agent
agent = Agent(
llm=LLM(config=llm_config),
config=agent_config,
)
# 5. Generate a session ID
sid = generate_sid(config)
# 6. Create Runtime
runtime = create_runtime(
config=config,
sid=sid,
headless_mode=True,
agent=agent,
)
# 7. Connect to the runtime
await runtime.connect()
# 8. Create Memory
memory = create_memory(
runtime=runtime,
event_stream=runtime.event_stream,
sid=sid,
)
# 9. Define the initial task
initial_user_action = MessageAction(content="Write a Python function that calculates the factorial of a number")
# 10. Run the agent
final_state = await run_controller(
config=config,
initial_user_action=initial_user_action,
sid=sid,
runtime=runtime,
agent=agent,
memory=memory,
headless_mode=True,
exit_on_message=True, # Exit when the agent asks for user input
)
# 11. Close the runtime
await runtime.close()
return final_state
# Run the async function
if __name__ == "__main__":
final_state = asyncio.run(run_openhands_agent())
print("Agent execution completed!")
```
## Components Overview
### AppConfig
The `AppConfig` class is the main configuration object for OpenHands. It contains settings for the runtime, agent, LLM, and more.
```python
from openhands.core.config import AppConfig
config = AppConfig(
runtime="local", # Options: "local", "docker", "e2b", "modal", etc.
file_store="memory", # Options: "memory", "local", etc.
file_store_path="/path/to/store", # Only needed for "local" file_store
max_iterations=100, # Maximum number of agent iterations
)
```
### LLMConfig
The `LLMConfig` class configures the language model used by the agent.
```python
from openhands.core.config import LLMConfig
llm_config = LLMConfig(
model="claude-sonnet-4-20250514", # Model name
api_key="your_api_key_here", # API key
temperature=0.0, # Temperature for generation
max_output_tokens=4096, # Maximum tokens in the response
)
```
### AgentConfig
The `AgentConfig` class configures the agent's behavior and available tools.
```python
from openhands.core.config import AgentConfig
agent_config = AgentConfig(
enable_browsing=True, # Enable web browsing
enable_cmd=True, # Enable bash commands
enable_editor=True, # Enable file editing
enable_jupyter=True, # Enable Jupyter notebook
enable_think=True, # Enable thinking tool
enable_finish=True, # Enable finish tool
)
```
### Agent
The `Agent` class represents the AI agent that will perform tasks.
```python
from openhands.controller.agent import Agent
from openhands.llm.llm import LLM
agent = Agent(
llm=LLM(config=llm_config),
config=agent_config,
)
```
### Runtime
The runtime is the environment where the agent executes commands and interacts with the system.
```python
from openhands.core.setup import create_runtime
runtime = create_runtime(
config=config,
sid=sid,
headless_mode=True,
agent=agent,
)
```
### Memory
The memory component manages the agent's context and conversation history.
```python
from openhands.core.setup import create_memory
memory = create_memory(
runtime=runtime,
event_stream=runtime.event_stream,
sid=sid,
)
```
## Advanced Usage
### Custom Sandbox Configuration
You can customize the sandbox environment by configuring the `SandboxConfig`:
```python
from openhands.core.config import SandboxConfig
sandbox_config = SandboxConfig(
selected_repo="username/repo", # GitHub repository to clone
base_image="ubuntu:22.04", # Base Docker image
)
config.sandbox = sandbox_config
```
### Security Configuration
Configure security settings using the `SecurityConfig`:
```python
from openhands.core.config import SecurityConfig
security_config = SecurityConfig(
confirmation_mode=False, # Whether to require confirmation for actions
security_analyzer="default", # Security analyzer to use
)
config.security = security_config
```
### Custom Agent Response Handling
You can provide a custom function to handle agent responses:
```python
def custom_response_handler(state):
# Process the agent's state and generate a response
return "Continue with your current approach"
final_state = await run_controller(
config=config,
initial_user_action=initial_user_action,
fake_user_response_fn=custom_response_handler,
)
```
## Building a Complete Application
Here's an example of a more complete application that uses OpenHands to assist with code generation:
```python
import asyncio
import os
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig, LLMConfig, AgentConfig, SandboxConfig
from openhands.events.action import MessageAction
from openhands.llm.llm import LLM
from openhands.core.setup import create_runtime, create_memory, generate_sid
from openhands.core.main import run_controller
from openhands.events import EventStreamSubscriber
from openhands.events.observation import AgentStateChangedObservation
from openhands.core.schema import AgentState
class CodeAssistant:
def __init__(self, api_key, model="claude-sonnet-4-20250514"):
self.api_key = api_key
self.model = model
self.config = None
self.agent = None
self.runtime = None
self.memory = None
self.sid = None
self.event_stream = None
async def initialize(self):
# Create configuration
self.config = AppConfig(
runtime="docker",
file_store="memory",
)
# Configure LLM
llm_config = LLMConfig(
model=self.model,
api_key=self.api_key,
temperature=0.0,
)
self.config.set_llm_config(llm_config)
# Configure Agent
agent_config = AgentConfig(
enable_browsing=True,
enable_cmd=True,
enable_editor=True,
enable_jupyter=True,
)
self.config.set_agent_config(agent_config)
# Configure Sandbox
sandbox_config = SandboxConfig(
base_image="ubuntu:22.04",
)
self.config.sandbox = sandbox_config
# Create Agent
self.agent = Agent(
llm=LLM(config=llm_config),
config=agent_config,
)
# Generate a session ID
self.sid = generate_sid(self.config)
# Create Runtime
self.runtime = create_runtime(
config=self.config,
sid=self.sid,
headless_mode=True,
agent=self.agent,
)
# Connect to the runtime
await self.runtime.connect()
# Create Memory
self.memory = create_memory(
runtime=self.runtime,
event_stream=self.runtime.event_stream,
sid=self.sid,
)
self.event_stream = self.runtime.event_stream
async def run_task(self, task_description, callback=None):
# Define the initial task
initial_user_action = MessageAction(content=task_description)
# Set up event callback if provided
if callback:
def on_event(event):
if isinstance(event, AgentStateChangedObservation):
callback(event)
self.event_stream.subscribe(
EventStreamSubscriber.MAIN,
on_event,
self.sid
)
# Run the agent
final_state = await run_controller(
config=self.config,
initial_user_action=initial_user_action,
sid=self.sid,
runtime=self.runtime,
agent=self.agent,
memory=self.memory,
headless_mode=True,
exit_on_message=True,
)
return final_state
async def close(self):
if self.runtime:
await self.runtime.close()
# Example usage
async def main():
# Initialize the code assistant
assistant = CodeAssistant(api_key=os.environ.get("ANTHROPIC_API_KEY"))
await assistant.initialize()
# Define a callback to process events
def event_callback(event):
if isinstance(event, AgentStateChangedObservation):
print(f"Agent state changed to: {event.agent_state}")
# Run a task
task = """
Create a simple Flask API with the following endpoints:
1. GET /users - Returns a list of users
2. GET /users/{id} - Returns a specific user
3. POST /users - Creates a new user
Use SQLite as the database and implement proper error handling.
"""
final_state = await assistant.run_task(task, callback=event_callback)
# Close the assistant
await assistant.close()
print("Task completed!")
if __name__ == "__main__":
asyncio.run(main())
```
## Conclusion
Using OpenHands as a library gives you the flexibility to integrate AI agents into your own applications. You can customize the agent's behavior, runtime environment, and how it interacts with your application.
For more advanced usage, refer to the OpenHands source code and API documentation. The library is highly customizable and can be adapted to a wide range of use cases.
@@ -7,6 +7,8 @@ The Cloud Issue Resolver automates code fixes and provides intelligent assistanc
The Cloud Issue Resolver is available automatically when you grant OpenHands Cloud repository access:
- [GitHub repository access](./github-installation#adding-repository-access)
![Adding repository access to OpenHands](/img/cloud/add-repo.png)
## Usage
After granting OpenHands Cloud repository access, you can use the Cloud Issue Resolver on issues and pull requests in your repositories.
@@ -20,6 +22,16 @@ On your repository, label an issue with `openhands` or add a message starting wi
2. Open a pull request if it determines that the issue has been successfully resolved
3. Comment on the issue with a summary of the performed tasks and a link to the PR
![OpenHands issue resolver in action](/img/cloud/issue-resolver.png)
#### Example Commands for Issues
Here are some examples of commands you can use with the issue resolver:
```
@openhands read the issue description and fix it
```
### Working with Pull Requests
To get OpenHands to work on pull requests, mention `@openhands` in comments to:
@@ -30,3 +42,15 @@ To get OpenHands to work on pull requests, mention `@openhands` in comments to:
OpenHands will:
1. Comment to let you know it is working on it
2. Perform the requested task
#### Example Commands for Pull Requests
Here are some examples of commands you can use with pull requests:
```
@openhands reflect the review comments
```
```
@openhands fix the merge conflicts and make sure that CI passes
```
@@ -36,6 +36,8 @@ You can grant OpenHands access to specific repositories:
- User's GitHub permissions (owner/collaborator)
3. Click `Install & Authorize`
![Adding repository access to OpenHands](/img/cloud/add-repo.png)
## Modifying Repository Access
You can modify repository access at any time by visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git` tab.
+11 -1
View File
@@ -66,9 +66,19 @@ The core configuration options are defined in the `[core]` section of the `confi
- `file_store`
- Type: `str`
- Default: `"memory"`
- Default: `"local"`
- Description: File store type
- `file_store_web_hook_url`
- Type: `str`
- Default: `None`
- Description: Optional url for a webhook to invoke after file store writes / deletes
- `file_store_web_hook_headers`
- Type: `str`
- Default: `None`
- Description: HTTP Headers to include in web hook requests.
- `file_uploads_allowed_extensions`
- Type: `list of str`
- Default: `[".*"]`
+1687 -1907
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -17,10 +17,10 @@
},
"// Note": "The OpenAPI spec is stored in docs/static/openapi.json so it's accessible at /openapi.json in the deployed site",
"dependencies": {
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-pages": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.0",
"@docusaurus/core": "^3.8.0",
"@docusaurus/plugin-content-pages": "^3.8.0",
"@docusaurus/preset-classic": "^3.8.0",
"@docusaurus/theme-mermaid": "^3.8.0",
"@mdx-js/react": "^3.1.0",
"@node-rs/jieba": "^2.0.1",
"clsx": "^2.0.0",
@@ -33,7 +33,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/tsconfig": "^3.8.0",
"@docusaurus/types": "^3.5.1",
"swagger-cli": "^4.0.4",
"swagger-ui-dist": "^5.22.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+3 -3
View File
@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -59,10 +59,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -39,11 +39,11 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-slim'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
@@ -24,7 +24,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
load_from_toml,
parse_arguments,
@@ -46,10 +46,10 @@ SKIP_NUM = (
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.11-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+3 -3
View File
@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -55,12 +55,12 @@ FILE_EXT_MAP = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0'
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -70,11 +70,11 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -33,13 +33,13 @@ SUPPORTED_AGENT_CLS = {'CodeActAgent'}
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
assert metadata.max_iterations == 1, (
'max_iterations must be 1 for browsing delegation evaluation.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -101,7 +101,7 @@ def get_instance_docker_image(repo_name: str) -> str:
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
repo_name = instance['repo'].split('/')[1]
base_container_image = get_instance_docker_image(repo_name)
logger.info(
@@ -113,7 +113,7 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -61,10 +61,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -47,10 +47,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -39,10 +39,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -37,7 +37,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -60,10 +60,10 @@ ACTION_FORMAT = """
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -81,10 +81,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -19,10 +19,10 @@ from evaluation.utils.shared import (
make_metadata,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -34,10 +34,10 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -53,7 +53,7 @@ def get_config(
return config
config = load_app_config()
config = load_openhands_config()
def load_bench_config():
@@ -29,10 +29,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -44,10 +44,10 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -63,7 +63,7 @@ def get_config(
return config
config = load_app_config()
config = load_openhands_config()
def load_bench_config():
@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -44,14 +44,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-logic-reasoning:v1.0'
sandbox_config.runtime_extra_deps = (
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -54,10 +54,10 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+3 -3
View File
@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -102,14 +102,14 @@ def load_incontext_example(task_name: str, with_tool: bool = True):
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-mint:v1.0'
sandbox_config.runtime_extra_deps = (
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -4,11 +4,11 @@ import pprint
import tqdm
from openhands.core.config import get_llm_config_arg, get_parser, load_app_config
from openhands.core.config import get_llm_config_arg, get_parser, load_openhands_config
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
config = load_app_config()
config = load_openhands_config()
def extract_test_results(res_file_path: str) -> tuple[list[str], list[str]]:
+5 -5
View File
@@ -33,10 +33,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -45,7 +45,7 @@ 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()
config = load_openhands_config()
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -76,10 +76,10 @@ ID2CONDA = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -28,8 +28,8 @@ from evaluation.utils.shared import (
run_evaluation,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
@@ -73,7 +73,7 @@ def process_git_patch(patch):
return patch
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -87,7 +87,7 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -314,7 +314,7 @@ def get_instance_docker_image(instance: pd.Series):
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
if USE_INSTANCE_IMAGE:
# We use a different instance image for the each instance of swe-bench eval
@@ -340,7 +340,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -58,12 +58,12 @@ def format_task_dict(example, use_knowledge):
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = (
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
@@ -24,8 +24,8 @@ from evaluation.utils.shared import (
run_evaluation,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
@@ -69,7 +69,7 @@ def process_git_patch(patch):
return patch
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -83,7 +83,7 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
+11 -8
View File
@@ -40,12 +40,12 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
@@ -220,7 +220,7 @@ def get_instance_docker_image(
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_swebench_official_image = 'swe-gym' not in metadata.dataset.lower()
base_container_image = get_instance_docker_image(
@@ -244,7 +244,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -721,15 +721,16 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
# repos for the swe-bench instances:
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
selected_repos = data['selected_repos']
if isinstance(selected_repos, str): selected_repos = [selected_repos]
if isinstance(selected_repos, str):
selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset["repo"].isin(selected_repos)]
subset = dataset[dataset['repo'].isin(selected_repos)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
@@ -806,7 +807,9 @@ if __name__ == '__main__':
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug('No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.')
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': args.mode}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -58,7 +58,7 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
_get_swebench_workspace_dir_name(instance)
instruction = f"""
Consider the following issue description:
@@ -168,7 +168,7 @@ def get_instance_docker_image(instance_id: str, official_image: bool = False) ->
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
@@ -197,7 +197,7 @@ def get_config(
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -348,13 +348,13 @@ def initialize_runtime(
# Check if an existing graph index file is available
graph_index_file_path = os.path.join(
INDEX_BASE_DIR, 'graph_index_v2.3', f"{instance['instance_id']}.pkl"
INDEX_BASE_DIR, 'graph_index_v2.3', f'{instance["instance_id"]}.pkl'
)
if INDEX_BASE_DIR and os.path.exists(graph_index_file_path):
logger.info(
f"Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3"
f'Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3'
)
runtime.copy_to(
graph_index_file_path,
f'/workspace/{workspace_dir_name}/_index_data/graph_index_v2.3',
@@ -364,9 +364,13 @@ def initialize_runtime(
)
obs = runtime.run_action(action)
bm25_index_dir = os.path.join(INDEX_BASE_DIR, 'BM25_index', instance['instance_id'])
bm25_index_dir = os.path.join(
INDEX_BASE_DIR, 'BM25_index', instance['instance_id']
)
runtime.copy_to(
bm25_index_dir, f'/workspace/{workspace_dir_name}/_index_data', recursive=True
bm25_index_dir,
f'/workspace/{workspace_dir_name}/_index_data',
recursive=True,
)
action = CmdRunAction(
command=f'mv _index_data/{instance["instance_id"]} _index_data/bm25_index'
@@ -41,7 +41,7 @@ from evaluation.utils.shared import (
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.core.config import AppConfig, SandboxConfig, get_parser
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_parser
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
from openhands.events.action import CmdRunAction
@@ -52,13 +52,13 @@ DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/kdja
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
def get_config(instance: pd.Series) -> AppConfig:
def get_config(instance: pd.Series) -> OpenHandsConfig:
base_container_image = get_instance_docker_image(instance['instance_id_swebench'])
assert base_container_image, (
f'Invalid container image for instance {instance["instance_id_swebench"]}.'
)
logger.info(f'Using instance container image: {base_container_image}.')
return AppConfig(
return OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
@@ -35,7 +35,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
SandboxConfig,
get_llm_config_arg,
get_parser,
@@ -117,7 +117,7 @@ def get_instance_docker_image(instance_id: str) -> str:
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of TestGenEval
base_container_image = get_instance_docker_image(instance['instance_id_swebench'])
logger.info(
@@ -126,7 +126,7 @@ def get_config(
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -15,8 +15,8 @@ from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_agent_config_arg,
get_llm_config_arg,
get_parser,
@@ -36,13 +36,13 @@ def get_config(
mount_path_on_host: str,
llm_config: LLMConfig,
agent_config: AgentConfig | None,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
max_budget_per_task=4,
max_iterations=100,
@@ -126,7 +126,7 @@ def codeact_user_response(state: State) -> str:
def run_solver(
runtime: Runtime,
task_name: str,
config: AppConfig,
config: OpenHandsConfig,
dependencies: list[str],
save_final_state: bool,
state_dir: str,
@@ -274,7 +274,7 @@ if __name__ == '__main__':
temp_dir = os.path.abspath(os.getenv('TMPDIR'))
else:
temp_dir = tempfile.mkdtemp()
config: AppConfig = get_config(
config: OpenHandsConfig = get_config(
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
)
runtime: Runtime = create_runtime(config)
+3 -3
View File
@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -40,10 +40,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -135,7 +135,7 @@ def get_instance_docker_image(instance_id: str, official_image: bool = False) ->
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
@@ -160,7 +160,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -48,7 +48,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
base_url = os.environ.get('VISUALWEBARENA_BASE_URL', None)
openai_api_key = os.environ.get('OPENAI_API_KEY', None)
openai_base_url = os.environ.get('OPENAI_BASE_URL', None)
@@ -72,7 +72,7 @@ def get_config(
'VWA_WIKIPEDIA': f'{base_url}:8888',
'VWA_HOMEPAGE': f'{base_url}:4399',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -44,7 +44,7 @@ SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
base_url = os.environ.get('WEBARENA_BASE_URL', None)
openai_api_key = os.environ.get('OPENAI_API_KEY', None)
assert base_url is not None, 'WEBARENA_BASE_URL must be set'
@@ -64,7 +64,7 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -41,10 +41,10 @@ FAKE_RESPONSES = {
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+2 -2
View File
@@ -2,9 +2,9 @@ import argparse
import pytest
from openhands.config import load_app_config
from openhands.config import load_openhands_config
config = load_app_config()
config = load_openhands_config()
if __name__ == '__main__':
"""Main entry point of the script.
@@ -7,6 +7,7 @@ import React from "react";
import { renderWithProviders } from "test-utils";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import OpenHands from "#/api/open-hands";
import { Conversation } from "#/api/open-hands.types";
describe("ConversationPanel", () => {
const onCloseMock = vi.fn();
@@ -37,11 +38,13 @@ describe("ConversationPanel", () => {
}));
});
const mockConversations = [
const mockConversations: Conversation[] = [
{
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
@@ -52,6 +55,8 @@ describe("ConversationPanel", () => {
conversation_id: "2",
title: "Conversation 2",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
@@ -62,6 +67,8 @@ describe("ConversationPanel", () => {
conversation_id: "3",
title: "Conversation 3",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
@@ -141,11 +148,13 @@ describe("ConversationPanel", () => {
it("should delete a conversation", async () => {
const user = userEvent.setup();
const mockData = [
const mockData: Conversation[] = [
{
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
@@ -156,6 +165,8 @@ describe("ConversationPanel", () => {
conversation_id: "2",
title: "Conversation 2",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
@@ -166,6 +177,8 @@ describe("ConversationPanel", () => {
conversation_id: "3",
title: "Conversation 3",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
@@ -8,6 +8,29 @@ import userEvent from "@testing-library/user-event";
import { HomeHeader } from "#/components/features/home/home-header";
import OpenHands from "#/api/open-hands";
// Mock the translation function
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string) => {
// Return a mock translation for the test
const translations: Record<string, string> = {
"HOME$LETS_START_BUILDING": "Let's start building",
"HOME$LAUNCH_FROM_SCRATCH": "Launch from Scratch",
"HOME$LOADING": "Loading...",
"HOME$OPENHANDS_DESCRIPTION": "OpenHands is an AI software engineer",
"HOME$NOT_SURE_HOW_TO_START": "Not sure how to start?",
"HOME$READ_THIS": "Read this"
};
return translations[key] || key;
},
i18n: { language: "en" },
}),
};
});
const renderHomeHeader = () => {
const RouterStub = createRoutesStub([
{
@@ -38,7 +61,7 @@ describe("HomeHeader", () => {
renderHomeHeader();
const launchButton = screen.getByRole("button", {
name: /launch from scratch/i,
name: /Launch from Scratch/i,
});
await userEvent.click(launchButton);
@@ -60,11 +83,11 @@ describe("HomeHeader", () => {
renderHomeHeader();
const launchButton = screen.getByRole("button", {
name: /launch from scratch/i,
name: /Launch from Scratch/i,
});
await userEvent.click(launchButton);
expect(launchButton).toHaveTextContent(/Loading/i);
expect(launchButton).toHaveTextContent(/Loading.../i);
expect(launchButton).toBeDisabled();
});
});
@@ -19,7 +19,11 @@ describe("Check for hardcoded English strings", () => {
const text = container.textContent;
// List of English strings that should be translated
const hardcodedStrings = ["What do you want to build?"];
const hardcodedStrings = [
"What do you want to build?",
"Launch from Scratch",
"Read this"
];
// Check each string
hardcodedStrings.forEach((str) => {
@@ -0,0 +1,42 @@
import { render } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { HomeHeader } from "#/components/features/home/home-header";
// Mock dependencies
vi.mock("#/hooks/mutation/use-create-conversation", () => ({
useCreateConversation: () => ({
mutate: vi.fn(),
isPending: false,
isSuccess: false,
}),
}));
vi.mock("#/hooks/use-is-creating-conversation", () => ({
useIsCreatingConversation: () => false,
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("Check for hardcoded English strings in Home components", () => {
test("HomeHeader should not have hardcoded English strings", () => {
const { container } = render(<HomeHeader />);
// Get all text content
const text = container.textContent;
// List of English strings that should be translated
const hardcodedStrings = [
"Launch from Scratch",
"Read this",
];
// Check each string
hardcodedStrings.forEach((str) => {
expect(text).not.toContain(str);
});
});
});
+2
View File
@@ -76,6 +76,8 @@ export interface Conversation {
conversation_id: string;
title: string;
selected_repository: string | null;
selected_branch: string | null;
git_provider: string | null;
last_updated_at: string;
created_at: string;
status: ProjectStatus;
@@ -26,7 +26,6 @@ import { downloadTrajectory } from "#/utils/download-trajectory";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import i18n from "#/i18n";
import { ErrorMessageBanner } from "./error-message-banner";
import { shouldRenderEvent } from "./event-content-helpers/should-render-event";
@@ -181,11 +180,7 @@ export function ChatInterface() {
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
</div>
{errorMessage && (
<ErrorMessageBanner
message={i18n.exists(errorMessage) ? t(errorMessage) : errorMessage}
/>
)}
{errorMessage && <ErrorMessageBanner message={errorMessage} />}
<InteractiveChatBox
onSubmit={handleSendMessage}
@@ -1,3 +1,7 @@
import { Trans } from "react-i18next";
import { Link } from "react-router";
import i18n from "#/i18n";
interface ErrorMessageBannerProps {
message: string;
}
@@ -5,7 +9,23 @@ interface ErrorMessageBannerProps {
export function ErrorMessageBanner({ message }: ErrorMessageBannerProps) {
return (
<div className="w-full rounded-lg p-2 text-black border border-red-800 bg-red-500">
{message}
{i18n.exists(message) ? (
<Trans
i18nKey={message}
components={{
a: (
<Link
className="underline font-bold cursor-pointer"
to="/settings/billing"
>
link
</Link>
),
}}
/>
) : (
message
)}
</div>
);
}
@@ -27,7 +27,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
return false;
},
[],
[messages],
);
return (
@@ -129,7 +129,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
isDisabled={isPending}
>
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL)
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
@@ -144,8 +144,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE) ||
"Submitting your feedback, please wait..."}
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
</p>
)}
</form>
@@ -31,7 +31,7 @@ export function HomeHeader() {
onClick={() => createConversation({})}
isDisabled={isCreatingConversation}
>
{!isCreatingConversation && "Launch from Scratch"}
{!isCreatingConversation && t("HOME$LAUNCH_FROM_SCRATCH")}
{isCreatingConversation && t("HOME$LOADING")}
</BrandButton>
</div>
@@ -48,7 +48,7 @@ export function HomeHeader() {
rel="noopener noreferrer"
className="underline underline-offset-2"
>
Read this
{t("HOME$READ_THIS")}
</a>
</p>
</div>
@@ -9,7 +9,6 @@ import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/open-hands.types";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
interface AuthModalProps {
githubAuthUrl: string | null;
@@ -26,10 +25,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITHUB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
@@ -37,10 +32,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITLAB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}
+12 -7
View File
@@ -166,6 +166,8 @@ export function WsClientProvider({
}
function handleMessage(event: Record<string, unknown>) {
handleAssistantMessage(event);
if (isOpenHandsEvent(event)) {
const isStatusUpdateError =
isStatusUpdate(event) && event.type === "error";
@@ -217,9 +219,14 @@ export function WsClientProvider({
isFileWriteAction(event) ||
isCommandAction(event)
) {
queryClient.removeQueries({
queryKey: ["file_changes", conversationId],
});
queryClient.invalidateQueries(
{
queryKey: ["file_changes", conversationId],
},
// Do not refetch if we are still receiving messages at a high rate (e.g., loading an existing conversation)
// This prevents unnecessary refetches when the user is still receiving messages
{ cancelRefetch: false },
);
// Invalidate file diff cache when a file is edited or written
if (!isCommandAction(event)) {
@@ -250,8 +257,6 @@ export function WsClientProvider({
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
}
handleAssistantMessage(event);
}
function handleDisconnect(data: unknown) {
@@ -284,14 +289,14 @@ export function WsClientProvider({
React.useEffect(() => {
lastEventRef.current = null;
}, [conversationId]);
React.useEffect(() => {
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
setStatus(WsClientProviderStatus.DISCONNECTED);
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
@@ -1,5 +1,4 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useConfig } from "../query/use-config";
@@ -8,7 +7,6 @@ import { clearLoginData } from "#/utils/local-storage";
export const useLogout = () => {
const queryClient = useQueryClient();
const { data: config } = useConfig();
const navigate = useNavigate();
return useMutation({
mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"),
@@ -24,7 +22,6 @@ export const useLogout = () => {
}
posthog.reset();
await navigate("/");
// Refresh the page after all logout logic is completed
window.location.reload();
+1 -1
View File
@@ -27,7 +27,7 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
apiSettings.enable_proactive_conversation_starters,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
SEARCH_API_KEY: apiSettings.search_api_key || "",
EMAIL: apiSettings.email || "",
MCP_CONFIG: apiSettings.mcp_config,
IS_NEW_USER: false,
};
+49
View File
@@ -0,0 +1,49 @@
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router";
import { useIsAuthed } from "./query/use-is-authed";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
import { useConfig } from "./query/use-config";
/**
* Hook to handle authentication callback and set login method after successful authentication
*/
export const useAuthCallback = () => {
const location = useLocation();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
const { data: config } = useConfig();
const navigate = useNavigate();
useEffect(() => {
// Only run in SAAS mode
if (config?.APP_MODE !== "saas") {
return;
}
// Wait for auth to load
if (isAuthLoading) {
return;
}
// Only set login method if authentication was successful
if (!isAuthed) {
return;
}
// Check if we have a login_method query parameter
const searchParams = new URLSearchParams(location.search);
const loginMethod = searchParams.get("login_method");
// Set the login method if it's valid
if (
loginMethod === LoginMethod.GITHUB ||
loginMethod === LoginMethod.GITLAB
) {
setLoginMethod(loginMethod as LoginMethod);
// Clean up the URL by removing the login_method parameter
searchParams.delete("login_method");
const newUrl = `${location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
navigate(newUrl, { replace: true });
}
}, [isAuthed, isAuthLoading, location.search, config?.APP_MODE]);
};
+5 -1
View File
@@ -53,8 +53,12 @@ export const useAutoLogin = () => {
// If we have an auth URL, redirect to it
if (authUrl) {
// Add the login method as a query parameter
const url = new URL(authUrl);
url.searchParams.append("login_method", loginMethod);
// After successful login, the user will be redirected back and can navigate to the last page
window.location.href = authUrl;
window.location.href = url.toString();
}
}, [
config?.APP_MODE,
+10 -2
View File
@@ -1,5 +1,7 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
HOME$READ_THIS = "HOME$READ_THIS",
AUTH$LOGGING_BACK_IN = "AUTH$LOGGING_BACK_IN",
SECURITY$LOW_RISK = "SECURITY$LOW_RISK",
SECURITY$MEDIUM_RISK = "SECURITY$MEDIUM_RISK",
@@ -228,8 +230,6 @@ export enum I18nKey {
FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",
@@ -550,4 +550,12 @@ export enum I18nKey {
TIPS$API_USAGE = "TIPS$API_USAGE",
TIPS$LEARN_MORE = "TIPS$LEARN_MORE",
TIPS$PROTIP = "TIPS$PROTIP",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
SETTINGS$NAV_USER = "SETTINGS$NAV_USER",
SETTINGS$USER_TITLE = "SETTINGS$USER_TITLE",
SETTINGS$USER_EMAIL = "SETTINGS$USER_EMAIL",
SETTINGS$USER_EMAIL_NOT_AVAILABLE = "SETTINGS$USER_EMAIL_NOT_AVAILABLE",
SETTINGS$SAVE = "SETTINGS$SAVE",
SETTINGS$EMAIL_SAVED_SUCCESSFULLY = "SETTINGS$EMAIL_SAVED_SUCCESSFULLY",
}
+159 -15
View File
@@ -1,4 +1,36 @@
{
"HOME$LAUNCH_FROM_SCRATCH": {
"en": "Launch from Scratch",
"ja": "ゼロから始める",
"zh-CN": "从零开始",
"zh-TW": "從零開始",
"ko-KR": "처음부터 시작",
"no": "Start fra bunnen",
"it": "Inizia da zero",
"pt": "Começar do zero",
"es": "Comenzar desde cero",
"ar": "البدء من الصفر",
"fr": "Démarrer de zéro",
"tr": "Sıfırdan başla",
"de": "Von Grund auf starten",
"uk": "Почати з нуля"
},
"HOME$READ_THIS": {
"en": "Read this",
"ja": "こちらを読む",
"zh-CN": "阅读此内容",
"zh-TW": "閱讀此內容",
"ko-KR": "이것을 읽어보세요",
"no": "Les dette",
"it": "Leggi questo",
"pt": "Leia isto",
"es": "Leer esto",
"ar": "اقرأ هذا",
"fr": "Lire ceci",
"tr": "Bunu oku",
"de": "Lies dies",
"uk": "Прочитайте це"
},
"AUTH$LOGGING_BACK_IN": {
"en": "Logging back into OpenHands...",
"ja": "OpenHandsに再ログインしています...",
@@ -6400,20 +6432,20 @@
"uk": "Запит не вдалося виконати через внутрішню помилку сервера."
},
"STATUS$ERROR_LLM_OUT_OF_CREDITS": {
"en": "You're out of OpenHands Credits",
"ja": "OpenHandsクレジットが不足しています",
"zh-CN": "您的OpenHands点数已用完",
"zh-TW": "您的OpenHands點數已用完",
"ko-KR": "OpenHands 크레딧이 소진되었습니다",
"no": "Du er tom for OpenHands-kreditter",
"it": "Hai esaurito i crediti OpenHands",
"pt": "Você está sem créditos OpenHands",
"es": "Te has quedado sin créditos de OpenHands",
"ar": "لقد نفدت رصيدك من OpenHands",
"fr": "Vous n'avez plus de crédits OpenHands",
"tr": "OpenHands kredileriniz tükendi",
"de": "Ihre OpenHands-Guthaben sind aufgebraucht",
"uk": "У вас закінчилися кредити OpenHands"
"en": "You're out of OpenHands Credits. <a>Add funds</a>",
"ja": "OpenHandsクレジットが不足しています。<a>資金を追加</a>",
"zh-CN": "您的OpenHands点数已用完。<a>添加资金</a>",
"zh-TW": "您的OpenHands點數已用完。<a>添加資金</a>",
"ko-KR": "OpenHands 크레딧이 소진되었습니다. <a>자금 추가</a>",
"no": "Du er tom for OpenHands-kreditter. <a>Legg til midler</a>",
"it": "Hai esaurito i crediti OpenHands. <a>Aggiungi fondi</a>",
"pt": "Você está sem créditos OpenHands. <a>Adicionar fundos</a>",
"es": "Te has quedado sin créditos de OpenHands. <a>Añadir fondos</a>",
"ar": "لقد نفدت رصيدك من OpenHands. <a>إضافة رصيد</a>",
"fr": "Vous n'avez plus de crédits OpenHands. <a>Ajouter des fonds</a>",
"tr": "OpenHands kredileriniz tükendi. <a>Bakiye ekle</a>",
"de": "Ihre OpenHands-Guthaben sind aufgebraucht. <a>Guthaben hinzufügen</a>",
"uk": "У вас закінчилися кредити OpenHands. <a>Додати кошти</a>"
},
"STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION": {
"en": "Content policy violation. The output was blocked by content filtering policy.",
@@ -8780,7 +8812,7 @@
"ar": "إرسال...",
"fr": "Envoi...",
"tr": "Gönderiliyor...",
"de": "Senden...",
"de": "Senden...",
"uk": "Відправляємо..."
},
"FEEDBACK$SUBMITTING_MESSAGE": {
@@ -8798,5 +8830,117 @@
"tr": "Geri bildirim gönderiliyor, lütfen bekleyin...",
"de": "Feedback senden, bitte warten...",
"uk": "Відправляємо відгук, будь ласка, почекайте..."
},
"SETTINGS$NAV_USER": {
"en": "User",
"ja": "ユーザー",
"zh-CN": "用户",
"zh-TW": "用戶",
"ko-KR": "사용자",
"no": "Bruker",
"it": "Utente",
"pt": "Usuário",
"es": "Usuario",
"ar": "المستخدم",
"fr": "Utilisateur",
"tr": "Kullanıcı",
"de": "Benutzer",
"uk": "Користувач"
},
"SETTINGS$USER_TITLE": {
"en": "User Information",
"ja": "ユーザー情報",
"zh-CN": "用户信息",
"zh-TW": "用戶信息",
"ko-KR": "사용자 정보",
"no": "Brukerinformasjon",
"it": "Informazioni utente",
"pt": "Informações do usuário",
"es": "Información del usuario",
"ar": "معلومات المستخدم",
"fr": "Informations utilisateur",
"tr": "Kullanıcı Bilgileri",
"de": "Benutzerinformationen",
"uk": "Інформація про користувача"
},
"SETTINGS$USER_EMAIL": {
"en": "Email",
"ja": "メール",
"zh-CN": "邮箱",
"zh-TW": "郵箱",
"ko-KR": "이메일",
"no": "E-post",
"it": "Email",
"pt": "Email",
"es": "Correo electrónico",
"ar": "البريد الإلكتروني",
"fr": "Email",
"tr": "E-posta",
"de": "E-Mail",
"uk": "Електронна пошта"
},
"SETTINGS$USER_EMAIL_NOT_AVAILABLE": {
"en": "Email not available",
"ja": "メールは利用できません",
"zh-CN": "邮箱不可用",
"zh-TW": "郵箱不可用",
"ko-KR": "이메일을 사용할 수 없습니다",
"no": "E-post ikke tilgjengelig",
"it": "Email non disponibile",
"pt": "Email não disponível",
"es": "Correo electrónico no disponible",
"ar": "البريد الإلكتروني غير متوفر",
"fr": "Email non disponible",
"tr": "E-posta mevcut değil",
"de": "E-Mail nicht verfügbar",
"uk": "Електронна пошта недоступна"
},
"SETTINGS$SAVE": {
"en": "Save",
"ja": "保存",
"zh-CN": "保存",
"zh-TW": "儲存",
"ko-KR": "저장",
"no": "Lagre",
"it": "Salva",
"pt": "Salvar",
"es": "Guardar",
"ar": "حفظ",
"fr": "Enregistrer",
"tr": "Kaydet",
"de": "Speichern",
"uk": "Зберегти"
},
"SETTINGS$EMAIL_SAVED_SUCCESSFULLY": {
"en": "Email saved successfully",
"ja": "メールが正常に保存されました",
"zh-CN": "邮箱保存成功",
"zh-TW": "郵箱儲存成功",
"ko-KR": "이메일이 성공적으로 저장되었습니다",
"no": "E-post lagret",
"it": "Email salvata con successo",
"pt": "Email salvo com sucesso",
"es": "Correo electrónico guardado con éxito",
"ar": "تم حفظ البريد الإلكتروني بنجاح",
"fr": "Email enregistré avec succès",
"tr": "E-posta başarıyla kaydedildi",
"de": "E-Mail erfolgreich gespeichert",
"uk": "Електронну пошту успішно збережено"
},
"SETTINGS$FAILED_TO_SAVE_EMAIL": {
"en": "Failed to save email",
"ja": "メールの保存に失敗しました",
"zh-CN": "保存邮箱失败",
"zh-TW": "儲存郵箱失敗",
"ko-KR": "이메일 저장 실패",
"no": "Kunne ikke lagre e-post",
"it": "Impossibile salvare l'email",
"pt": "Falha ao salvar email",
"es": "Error al guardar el correo electrónico",
"ar": "فشل في حفظ البريد الإلكتروني",
"fr": "Échec de l'enregistrement de l'email",
"tr": "E-posta kaydedilemedi",
"de": "E-Mail konnte nicht gespeichert werden",
"uk": "Не вдалося зберегти електронну пошту"
}
}
+8
View File
@@ -52,6 +52,8 @@ const conversations: Conversation[] = [
conversation_id: "1",
title: "My New Project",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
@@ -62,6 +64,8 @@ const conversations: Conversation[] = [
conversation_id: "2",
title: "Repo Testing",
selected_repository: "octocat/hello-world",
git_provider: "github",
selected_branch: null,
// 2 days ago
last_updated_at: new Date(
Date.now() - 2 * 24 * 60 * 60 * 1000,
@@ -75,6 +79,8 @@ const conversations: Conversation[] = [
conversation_id: "3",
title: "Another Project",
selected_repository: "octocat/earth",
git_provider: null,
selected_branch: "main",
// 5 days ago
last_updated_at: new Date(
Date.now() - 5 * 24 * 60 * 60 * 1000,
@@ -271,6 +277,8 @@ export const handlers = [
conversation_id: (Math.random() * 100).toString(),
title: "New Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
+1
View File
@@ -12,6 +12,7 @@ export default [
route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"),
route("mcp", "routes/mcp-settings.tsx"),
route("user", "routes/user-settings.tsx"),
route("git", "routes/git-settings.tsx"),
route("app", "routes/app-settings.tsx"),
route("billing", "routes/billing.tsx"),
+4 -2
View File
@@ -36,6 +36,7 @@ import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
function AppContent() {
useConversationConfig();
@@ -43,6 +44,7 @@ function AppContent() {
const { data: settings } = useSettings();
const { conversationId } = useConversationId();
const { data: conversation, isFetched } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
@@ -54,13 +56,13 @@ function AppContent() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
if (isFetched && !conversation) {
if (isFetched && !conversation && isAuthed) {
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
navigate("/");
}
}, [conversation, isFetched]);
}, [conversation, isFetched, isAuthed]);
React.useEffect(() => {
dispatch(clearTerminal());
+39 -2
View File
@@ -23,6 +23,7 @@ import { SetupPaymentModal } from "#/components/features/payment/setup-payment-m
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
export function ErrorBoundary() {
@@ -88,6 +89,9 @@ export default function MainApp() {
// Auto-login if login method is stored in local storage
useAutoLogin();
// Handle authentication callback and set login method after successful authentication
useAuthCallback();
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {
@@ -131,8 +135,8 @@ export default function MainApp() {
}
}, [error?.status, pathname, isOnTosPage]);
// Check if login method exists in local storage
const loginMethodExists = React.useMemo(() => {
// Function to check if login method exists in local storage
const checkLoginMethodExists = React.useCallback(() => {
// Only check localStorage if we're in a browser environment
if (typeof window !== "undefined" && window.localStorage) {
return localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD) !== null;
@@ -140,6 +144,39 @@ export default function MainApp() {
return false;
}, []);
// State to track if login method exists
const [loginMethodExists, setLoginMethodExists] = React.useState(
checkLoginMethodExists(),
);
// Listen for storage events to update loginMethodExists when logout happens
React.useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === LOCAL_STORAGE_KEYS.LOGIN_METHOD) {
setLoginMethodExists(checkLoginMethodExists());
}
};
// Also check on window focus, as logout might happen in another tab
const handleWindowFocus = () => {
setLoginMethodExists(checkLoginMethodExists());
};
window.addEventListener("storage", handleStorageChange);
window.addEventListener("focus", handleWindowFocus);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener("focus", handleWindowFocus);
};
}, [checkLoginMethodExists]);
// Check login method status when auth status changes
React.useEffect(() => {
// When auth status changes (especially on logout), recheck login method
setLoginMethodExists(checkLoginMethodExists());
}, [isAuthed, checkLoginMethodExists]);
const renderAuthModal =
!isAuthed &&
!isAuthError &&
+3 -1
View File
@@ -15,6 +15,7 @@ function SettingsScreen() {
const isSaas = config?.APP_MODE === "saas";
const saasNavItems = [
{ to: "/settings/user", text: t("SETTINGS$NAV_USER") },
{ to: "/settings/git", text: t("SETTINGS$NAV_GIT") },
{ to: "/settings/app", text: t("SETTINGS$NAV_APPLICATION") },
{ to: "/settings/billing", text: t("SETTINGS$NAV_CREDITS") },
@@ -33,10 +34,11 @@ function SettingsScreen() {
React.useEffect(() => {
if (isSaas) {
if (pathname === "/settings") {
navigate("/settings/git");
navigate("/settings/user");
}
} else {
const noEnteringPaths = [
"/settings/user",
"/settings/billing",
"/settings/credits",
"/settings/api-keys",
+98
View File
@@ -0,0 +1,98 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { useSettings } from "#/hooks/query/use-settings";
import { openHands } from "#/api/open-hands-axios";
function UserSettingsScreen() {
const { t } = useTranslation();
const { data: settings, isLoading } = useSettings();
const [email, setEmail] = useState("");
const [originalEmail, setOriginalEmail] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const queryClient = useQueryClient();
useEffect(() => {
if (settings?.EMAIL) {
setEmail(settings.EMAIL);
setOriginalEmail(settings.EMAIL);
}
}, [settings?.EMAIL]);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setSaveSuccess(false);
};
const handleSaveEmail = async () => {
if (email === originalEmail) return;
try {
setIsSaving(true);
// Send email as part of settings update
await openHands.post('/api/settings', { email });
setOriginalEmail(email);
setSaveSuccess(true);
// Invalidate settings query to refresh data
queryClient.invalidateQueries({ queryKey: ["settings"] });
} catch (error) {
// Log error but don't show to user
// eslint-disable-next-line no-console
console.error(t("SETTINGS$FAILED_TO_SAVE_EMAIL"), error);
} finally {
setIsSaving(false);
}
};
const isEmailChanged = email !== originalEmail;
return (
<div data-testid="user-settings-screen" className="flex flex-col h-full">
<div className="p-9 flex flex-col gap-6">
{isLoading ? (
<div className="animate-pulse h-8 w-64 bg-tertiary rounded" />
) : (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm">{t("SETTINGS$USER_EMAIL")}</label>
<div className="flex items-center gap-3">
<input
type="email"
value={email}
onChange={handleEmailChange}
className="text-base text-primary p-2 bg-base-tertiary rounded border border-tertiary flex-grow"
placeholder={t("SETTINGS$USER_EMAIL_NOT_AVAILABLE")}
data-testid="email-input"
/>
<button
type="button"
onClick={handleSaveEmail}
disabled={!isEmailChanged || isSaving}
className={`px-4 py-2 rounded ${
isEmailChanged && !isSaving
? "bg-primary text-white hover:bg-primary-dark"
: "bg-tertiary text-secondary cursor-not-allowed"
}`}
data-testid="save-email-button"
>
{isSaving ? t("SETTINGS$SAVING") : t("SETTINGS$SAVE")}
</button>
</div>
{saveSuccess && (
<div className="text-sm text-green-500 mt-1">
{t("SETTINGS$EMAIL_SAVED_SUCCESSFULLY")}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
export default UserSettingsScreen;
+2
View File
@@ -45,6 +45,7 @@ export type Settings = {
SEARCH_API_KEY?: string;
IS_NEW_USER?: boolean;
MCP_CONFIG?: MCPConfig;
EMAIL?: string;
};
export type ApiSettings = {
@@ -68,6 +69,7 @@ export type ApiSettings = {
sse_servers: (string | MCPSSEServer)[];
stdio_servers: MCPStdioServer[];
};
email?: string;
};
export type PostSettings = Settings & {
+4 -1
View File
@@ -16,5 +16,8 @@ export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
authUrl = `auth.${requestUrl.hostname}`;
}
const scope = "openid email profile"; // OAuth scope - not user-facing
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(requestUrl.href)}`;
const separator = requestUrl.search ? "&" : "?";
const cleanHref = requestUrl.href.replace(/\/$/, "");
const state = `${cleanHref}${separator}login_method=${identityProvider}`;
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`;
};
+4 -5
View File
@@ -11,10 +11,11 @@ triggers:
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
the GitHub API.
<IMPORTANT>
You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
ALWAYS use the GitHub API for operations instead of a web browser.
To open a pull request, always use the `create_pr` tool
ALWAYS use the `create_pr` tool to open a pull request
</IMPORTANT>
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
@@ -26,11 +27,9 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a pull request, send the user a short message with a link to the pull request.
* Prefer "Draft" pull requests when possible
* Do NOT mark a pull request as ready to review unless the user explicitly says so
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
# Then use the MCP tool to create the PR instead of directly using the GitHub API
```
+4 -5
View File
@@ -11,10 +11,11 @@ triggers:
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
the GitLab API.
<IMPORTANT>
You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
ALWAYS use the GitLab API for operations instead of a web browser.
To open a merge request, always use the `create_mr` tool
ALWAYS use the `create_mr` tool to open a merge request
</IMPORTANT>
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`
@@ -26,10 +27,8 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a merge request, send the user a short message with a link to the merge request.
* Prefer "Draft" merge requests when possible
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
# Then use the MCP tool to create the MR instead of directly using the GitLab API
```
@@ -266,5 +266,6 @@ class CodeActAgent(Agent):
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return codeact_function_calling.response_to_actions(
response, mcp_tool_names=list(self.mcp_tools.keys()),
response,
mcp_tool_names=list(self.mcp_tools.keys()),
)
@@ -5,14 +5,13 @@ This is similar to the functionality of `CodeActResponseParser`.
import json
from litellm import (
ChatCompletionToolParam,
ModelResponse,
)
from openhands.agenthub.codeact_agent.tools import FinishTool
from openhands.agenthub.codeact_agent.function_calling import combine_thought
from openhands.agenthub.codeact_agent.tools import FinishTool
from openhands.agenthub.loc_agent.tools import (
SearchEntityTool,
SearchRepoTool,
@@ -32,7 +31,8 @@ from openhands.events.tool import ToolCallMetadata
def response_to_actions(
response: ModelResponse, mcp_tool_names: list[str] | None = None,
response: ModelResponse,
mcp_tool_names: list[str] | None = None,
) -> list[Action]:
actions: list[Action] = []
assert len(response.choices) == 1, 'Only one choice is supported for now'
@@ -87,7 +87,7 @@ def response_to_actions(
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
)
# We only add thought to the first action
if i == 0:
action = combine_thought(action, thought)
@@ -106,7 +106,7 @@ def response_to_actions(
wait_for_response=True,
)
)
# Add response id to actions
# This will ensure we can match both actions without tool calls (e.g. MessageAction)
# and actions with tool calls (e.g. CmdRunAction, IPythonRunCellAction, etc.)
@@ -116,7 +116,7 @@ def response_to_actions(
assert len(actions) >= 1
return actions
def get_tools() -> list[ChatCompletionToolParam]:
tools = [FinishTool]
+5 -5
View File
@@ -1,13 +1,12 @@
from openhands.agenthub.codeact_agent import CodeActAgent
from typing import TYPE_CHECKING
import openhands.agenthub.loc_agent.function_calling as locagent_function_calling
from openhands.agenthub.codeact_agent import CodeActAgent
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.events.action import Action
from openhands.llm.llm import ModelResponse
@@ -35,5 +34,6 @@ class LocAgent(CodeActAgent):
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return locagent_function_calling.response_to_actions(
response, mcp_tool_names=list(self.mcp_tools.keys()),
response,
mcp_tool_names=list(self.mcp_tools.keys()),
)
+5 -5
View File
@@ -25,7 +25,7 @@ from openhands.cli.utils import (
write_to_file,
)
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
)
from openhands.core.schema import AgentState
from openhands.events import EventSource
@@ -42,7 +42,7 @@ async def handle_commands(
event_stream: EventStream,
usage_metrics: UsageMetrics,
sid: str,
config: AppConfig,
config: OpenHandsConfig,
current_dir: str,
settings_store: FileSettingsStore,
) -> tuple[bool, bool, bool]:
@@ -105,7 +105,7 @@ def handle_help_command() -> None:
async def handle_init_command(
config: AppConfig, event_stream: EventStream, current_dir: str
config: OpenHandsConfig, event_stream: EventStream, current_dir: str
) -> tuple[bool, bool]:
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
@@ -166,7 +166,7 @@ def handle_new_command(
async def handle_settings_command(
config: AppConfig,
config: OpenHandsConfig,
settings_store: FileSettingsStore,
) -> None:
display_settings(config)
@@ -264,7 +264,7 @@ async def init_repository(current_dir: str) -> bool:
return init_repo
def check_folder_security_agreement(config: AppConfig, current_dir: str) -> bool:
def check_folder_security_agreement(config: OpenHandsConfig, current_dir: str) -> bool:
# Directories trusted by user for the CLI to use as workspace
# Config from ~/.openhands/config.toml overrides the app config
+3 -3
View File
@@ -30,7 +30,7 @@ from openhands.cli.utils import (
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
parse_arguments,
setup_config_from_args,
)
@@ -103,7 +103,7 @@ async def cleanup_session(
async def run_session(
loop: asyncio.AbstractEventLoop,
config: AppConfig,
config: OpenHandsConfig,
settings_store: FileSettingsStore,
current_dir: str,
task_content: str | None = None,
@@ -334,7 +334,7 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
logger.setLevel(logging.WARNING)
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
config: OpenHandsConfig = setup_config_from_args(args)
# Load settings from Settings Store
# TODO: Make this generic?
+4 -4
View File
@@ -18,7 +18,7 @@ from openhands.cli.utils import (
organize_models_and_providers,
)
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import OH_DEFAULT_AGENT
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
@@ -29,7 +29,7 @@ from openhands.storage.settings.file_settings_store import FileSettingsStore
from openhands.utils.llm import get_supported_llm_models
def display_settings(config: AppConfig) -> None:
def display_settings(config: OpenHandsConfig) -> None:
llm_config = config.get_llm_config()
advanced_llm_settings = True if llm_config.base_url else False
@@ -145,7 +145,7 @@ def save_settings_confirmation() -> bool:
async def modify_llm_settings_basic(
config: AppConfig, settings_store: FileSettingsStore
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
model_list = get_supported_llm_models(config)
organized_models = organize_models_and_providers(model_list)
@@ -243,7 +243,7 @@ async def modify_llm_settings_basic(
async def modify_llm_settings_advanced(
config: AppConfig, settings_store: FileSettingsStore
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
session = PromptSession(key_bindings=kb_cancel())
+2 -2
View File
@@ -27,7 +27,7 @@ from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from openhands import __version__
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.schema import AgentState
from openhands.events import EventSource, EventStream
from openhands.events.action import (
@@ -180,7 +180,7 @@ def display_initial_user_prompt(prompt: str) -> None:
# Prompt output display functions
def display_event(event: Event, config: AppConfig) -> None:
def display_event(event: Event, config: OpenHandsConfig) -> None:
global streaming_output_text_area
with print_lock:
if isinstance(event, Action):
+19 -14
View File
@@ -5,7 +5,7 @@ import copy
import os
import time
import traceback
from typing import Callable, ClassVar
from typing import Callable
import litellm # noqa
from litellm.exceptions import ( # noqa
@@ -61,6 +61,7 @@ from openhands.events.action import (
)
from openhands.events.action.agent import CondensationAction, RecallAction
from openhands.events.event import Event
from openhands.events.event_filter import EventFilter
from openhands.events.observation import (
AgentDelegateObservation,
AgentStateChangedObservation,
@@ -93,12 +94,6 @@ class AgentController:
delegate: 'AgentController | None' = None
_pending_action_info: tuple[Action, float] | None = None # (action, timestamp)
_closed: bool = False
filter_out: ClassVar[tuple[type[Event], ...]] = (
NullAction,
NullObservation,
ChangeAgentStateAction,
AgentStateChangedObservation,
)
_cached_first_user_message: MessageAction | None = None
def __init__(
@@ -150,6 +145,18 @@ class AgentController:
EventStreamSubscriber.AGENT_CONTROLLER, self.on_event, self.id
)
# filter out events that are not relevant to the agent
# so they will not be included in the agent history
self.agent_history_filter = EventFilter(
exclude_types=(
NullAction,
NullObservation,
ChangeAgentStateAction,
AgentStateChangedObservation,
),
exclude_hidden=True,
)
# state from the previous session, state from a parent agent, or a fresh state
self.set_initial_state(
state=initial_state,
@@ -219,12 +226,11 @@ class AgentController:
else self.event_stream.get_latest_event_id()
)
self.state.history = list(
self.event_stream.get_events(
self.event_stream.search_events(
start_id=start_id,
end_id=end_id,
reverse=False,
filter_out_type=self.filter_out,
filter_hidden=True,
filter=self.agent_history_filter,
)
)
@@ -406,7 +412,7 @@ class AgentController:
return
# if the event is not filtered out, add it to the history
if not any(isinstance(event, filter_type) for filter_type in self.filter_out):
if self.agent_history_filter.include(event):
self.state.history.append(event)
if isinstance(event, Action):
@@ -1091,12 +1097,11 @@ class AgentController:
# Get rest of history
events_to_add = list(
self.event_stream.get_events(
self.event_stream.search_events(
start_id=start_id,
end_id=end_id,
reverse=False,
filter_out_type=self.filter_out,
filter_hidden=True,
filter=self.agent_history_filter,
)
)
events.extend(events_to_add)
+4 -4
View File
@@ -1,5 +1,4 @@
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
@@ -8,6 +7,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.core.config.utils import (
@@ -15,9 +15,9 @@ from openhands.core.config.utils import (
get_agent_config_arg,
get_llm_config_arg,
get_parser,
load_app_config,
load_from_env,
load_from_toml,
load_openhands_config,
parse_arguments,
setup_config_from_args,
)
@@ -26,13 +26,13 @@ __all__ = [
'OH_DEFAULT_AGENT',
'OH_MAX_ITERATIONS',
'AgentConfig',
'AppConfig',
'OpenHandsConfig',
'MCPConfig',
'LLMConfig',
'SandboxConfig',
'SecurityConfig',
'ExtendedConfig',
'load_app_config',
'load_openhands_config',
'load_from_env',
'load_from_toml',
'finalize_config',
+6 -6
View File
@@ -1,10 +1,11 @@
import os
from urllib.parse import urlparse
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from pydantic import BaseModel, Field, ValidationError, model_validator
if TYPE_CHECKING:
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.utils.import_utils import get_impl
@@ -147,7 +148,7 @@ class MCPConfig(BaseModel):
class OpenHandsMCPConfig:
@staticmethod
def add_search_engine(app_config: "AppConfig") -> MCPStdioServerConfig | None:
def add_search_engine(app_config: 'OpenHandsConfig') -> MCPStdioServerConfig | None:
"""Add search engine to the MCP config"""
if (
app_config.search_api_key
@@ -165,17 +166,16 @@ class OpenHandsMCPConfig:
# Do not add search engine to MCP config in SaaS mode since it will be added by the OpenHands server
return None
@staticmethod
def create_default_mcp_server_config(
host: str, config: "AppConfig", user_id: str | None = None
host: str, config: 'OpenHandsConfig', user_id: str | None = None
) -> tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]:
"""
Create a default MCP server configuration.
Args:
host: Host string
config: AppConfig
config: OpenHandsConfig
Returns:
tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]: A tuple containing the default SSE server configuration and a list of MCP stdio server configurations
"""
@@ -16,7 +16,7 @@ from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
class AppConfig(BaseModel):
class OpenHandsConfig(BaseModel):
"""Configuration for the app.
Attributes:
@@ -29,6 +29,8 @@ class AppConfig(BaseModel):
runtime: Runtime environment identifier.
file_store: Type of file store to use.
file_store_path: Path to the file store.
file_store_web_hook_url: Optional url for file store web hook
file_store_web_hook_headers: Optional headers for file_store web hook
save_trajectory_path: Either a folder path to store trajectories with auto-generated filenames, or a designated trajectory file path.
save_screenshots_in_trajectory: Whether to save screenshots in trajectory (in encoded image format).
replay_trajectory_path: Path to load trajectory and replay. If provided, trajectory would be replayed first before user's instruction.
@@ -62,10 +64,15 @@ class AppConfig(BaseModel):
runtime: str = Field(default='docker')
file_store: str = Field(default='local')
file_store_path: str = Field(default='/tmp/openhands_file_store')
file_store_web_hook_url: str | None = Field(default=None)
file_store_web_hook_headers: dict | None = Field(default=None)
save_trajectory_path: str | None = Field(default=None)
save_screenshots_in_trajectory: bool = Field(default=False)
replay_trajectory_path: str | None = Field(default=None)
search_api_key: SecretStr | None = Field(default=None, description="API key for Tavily search engine (https://tavily.com/). Required for search functionality.")
search_api_key: SecretStr | None = Field(
default=None,
description='API key for Tavily search engine (https://tavily.com/). Required for search functionality.',
)
# Deprecated parameters - will be removed in a future version
workspace_base: str | None = Field(default=None, deprecated=True)
@@ -73,7 +80,7 @@ class AppConfig(BaseModel):
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
# End of deprecated parameters
cache_dir: str = Field(default='/tmp/cache')
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
@@ -148,5 +155,5 @@ class AppConfig(BaseModel):
"""Post-initialization hook, called when the instance is created with only default values."""
super().model_post_init(__context)
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
AppConfig.defaults_dict = model_defaults_to_dict(self)
if not OpenHandsConfig.defaults_dict: # Only set defaults_dict if it's empty
OpenHandsConfig.defaults_dict = model_defaults_to_dict(self)
+12 -12
View File
@@ -15,7 +15,6 @@ 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 (
CondenserConfig,
condenser_config_from_toml_section,
@@ -28,6 +27,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.storage import get_file_store
@@ -39,7 +39,7 @@ load_dotenv()
def load_from_env(
cfg: AppConfig, env_or_toml_dict: dict | MutableMapping[str, str]
cfg: OpenHandsConfig, env_or_toml_dict: dict | MutableMapping[str, str]
) -> None:
"""Sets config attributes from environment variables or TOML dictionary.
@@ -48,7 +48,7 @@ def load_from_env(
(e.g., AGENT_MEMORY_ENABLED), sandbox settings (e.g., SANDBOX_TIMEOUT), and more.
Args:
cfg: The AppConfig object to set attributes on.
cfg: The OpenHandsConfig object to set attributes on.
env_or_toml_dict: The environment variables or a config.toml dict.
"""
@@ -121,11 +121,11 @@ def load_from_env(
set_attr_from_env(default_agent_config, 'AGENT_')
def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
def load_from_toml(cfg: OpenHandsConfig, toml_file: str = 'config.toml') -> None:
"""Load the config from the toml file. Supports both styles of config vars.
Args:
cfg: The AppConfig object to update attributes of.
cfg: The OpenHandsConfig object to update attributes of.
toml_file: The path to the toml file. Defaults to 'config.toml'.
See Also:
@@ -302,7 +302,7 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
return new_secret
def finalize_config(cfg: AppConfig) -> None:
def finalize_config(cfg: OpenHandsConfig) -> None:
"""More tweaks to the config after it's been loaded."""
# Handle the sandbox.volumes parameter
if cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
@@ -759,7 +759,7 @@ def parse_arguments() -> argparse.Namespace:
return args
def register_custom_agents(config: AppConfig) -> None:
def register_custom_agents(config: OpenHandsConfig) -> None:
"""Register custom agents from configuration.
This function is called after configuration is loaded to ensure all custom agents
@@ -782,16 +782,16 @@ def register_custom_agents(config: AppConfig) -> None:
)
def load_app_config(
def load_openhands_config(
set_logging_levels: bool = True, config_file: str = 'config.toml'
) -> AppConfig:
) -> OpenHandsConfig:
"""Load the configuration from the specified config file and environment variables.
Args:
set_logging_levels: Whether to set the global variables for logging levels.
config_file: Path to the config file. Defaults to 'config.toml' in the current directory.
"""
config = AppConfig()
config = OpenHandsConfig()
load_from_toml(config, config_file)
load_from_env(config, os.environ)
finalize_config(config)
@@ -802,13 +802,13 @@ def load_app_config(
return config
def setup_config_from_args(args: argparse.Namespace) -> AppConfig:
def setup_config_from_args(args: argparse.Namespace) -> OpenHandsConfig:
"""Load config from toml and override with command line arguments.
Common setup used by both CLI and main.py entry points.
"""
# Load base config from toml and env vars
config = load_app_config(config_file=args.config_file)
config = load_openhands_config(config_file=args.config_file)
# Override with command line arguments if provided
if args.llm_config:
+4 -4
View File
@@ -9,7 +9,7 @@ from openhands.controller.agent import Agent
from openhands.controller.replay import ReplayManager
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
parse_arguments,
setup_config_from_args,
)
@@ -47,7 +47,7 @@ class FakeUserResponseFunc(Protocol):
async def run_controller(
config: AppConfig,
config: OpenHandsConfig,
initial_user_action: Action,
sid: str | None = None,
runtime: Runtime | None = None,
@@ -90,7 +90,7 @@ async def run_controller(
config.max_budget_per_task.
Example:
>>> config = load_app_config()
>>> config = load_openhands_config()
>>> action = MessageAction(content="Write a hello world program")
>>> state = await run_controller(config=config, initial_user_action=action)
"""
@@ -279,7 +279,7 @@ def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]:
if __name__ == '__main__':
args = parse_arguments()
config: AppConfig = setup_config_from_args(args)
config: OpenHandsConfig = setup_config_from_args(args)
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
+5 -5
View File
@@ -10,7 +10,7 @@ from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
@@ -28,7 +28,7 @@ from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
def create_runtime(
config: AppConfig,
config: OpenHandsConfig,
sid: str | None = None,
headless_mode: bool = True,
agent: Agent | None = None,
@@ -172,7 +172,7 @@ def create_memory(
return memory
def create_agent(config: AppConfig) -> Agent:
def create_agent(config: OpenHandsConfig) -> Agent:
agent_cls: type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
@@ -188,7 +188,7 @@ def create_agent(config: AppConfig) -> Agent:
def create_controller(
agent: Agent,
runtime: Runtime,
config: AppConfig,
config: OpenHandsConfig,
headless_mode: bool = True,
replay_events: list[Event] | None = None,
) -> tuple[AgentController, State | None]:
@@ -218,7 +218,7 @@ def create_controller(
return (controller, initial_state)
def generate_sid(config: AppConfig, session_name: str | None = None) -> str:
def generate_sid(config: OpenHandsConfig, session_name: str | None = None) -> str:
"""Generate a session id based on the session name and the jwt secret."""
session_name = session_name or str(uuid.uuid4())
jwt_secret = config.jwt_secret
+3 -1
View File
@@ -11,7 +11,9 @@ class MCPObservation(Observation):
observation: str = ObservationType.MCP
name: str = '' # The name of the MCP tool that was called
arguments: dict[str, Any] = field(default_factory=dict) # The arguments passed to the MCP tool
arguments: dict[str, Any] = field(
default_factory=dict
) # The arguments passed to the MCP tool
@property
def message(self) -> str:
@@ -12,7 +12,7 @@ A comment on the issue has been addressed to you.
When you're done, make sure to
1. Re-read the issue title, body, and comments and make sure that you have successfully implemented all requirements.
2. Use the `GITHUB_TOKEN` environment variable and GitHub API to open a new PR
2. Use the `create_pr` tool to open a new PR
3. Name the branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
4. The PR description should mention that it "fixes" or "closes" the issue number
5. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})`
@@ -1 +1 @@
{{ issue_comment }}
{{ issue_comment }}
@@ -7,6 +7,6 @@ Your tasking is to fix an issue in your repository. Do the following
When you're done, make sure to
1. Use the `GITHUB_TOKEN` environment variable and GitHub API to open a new PR
1. Use the `create_pr` tool to open a new PR
2. The PR description should mention that it "fixes" or "closes" the issue number
3. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})`

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