mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 489e32c2c0 | |||
| c189012f0a | |||
| 2407420e17 | |||
| bb0c47c41a | |||
| 83e5276de5 | |||
| 816082a55b | |||
| 82d72b145d | |||
| 277b87413b | |||
| 972837ff73 | |||
| f8c3470c91 | |||
| de196d6da1 | |||
| 10a4ab9199 | |||
| 3f0d8e7f47 | |||
| 0e0687efc3 | |||
| 30d53f8df0 | |||
| fd52841842 | |||
| d770edc010 | |||
| df2374bd85 | |||
| 542be7338e | |||
| e4d84946db | |||
| cc881a6bcb | |||
| cb0a1c91e4 | |||
| e208bffade | |||
| 6491142364 | |||
| 205f0234e8 | |||
| c76809a766 | |||
| 9f86f731a7 | |||
| 6fe5da810b | |||
| 52a1e94335 | |||
| 3e0532e8b9 | |||
| 90c440d709 | |||
| 82657b7ba1 | |||
| 3c51600260 | |||
| b5f2a04ea2 | |||
| 155615bbb1 |
+14
-1
@@ -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
@@ -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.
|
||||
+25
-1
@@ -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)
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
#### 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`
|
||||
|
||||

|
||||
|
||||
## Modification de l'Accès au Dépôt
|
||||
|
||||
Vous pouvez modifier l'accès au dépôt à tout moment :
|
||||
|
||||
+25
-1
@@ -8,18 +8,30 @@
|
||||
- [GitHubリポジトリアクセス](./github-installation#adding-repository-access)
|
||||
- [GitLabリポジトリアクセス](./gitlab-installation#adding-repository-access)
|
||||
|
||||

|
||||
|
||||
## 使用方法
|
||||
|
||||
OpenHands Cloudリポジトリアクセスを許可した後、リポジトリの課題やプルリクエスト/マージリクエストでクラウド課題リゾルバーを使用できます。
|
||||
|
||||
### 課題の操作
|
||||
|
||||
リポジトリで、課題に`openhands`というラベルを付けます。OpenHandsは以下を行います:
|
||||
リポジトリで、課題に`openhands`というラベルを付けるか、`@openhands`で始まるメッセージを追加します。OpenHandsは以下を行います:
|
||||
1. 課題にコメントして、作業中であることを知らせます
|
||||
- リンクをクリックすると、OpenHands Cloudで進捗状況を追跡できます
|
||||
2. 課題が正常に解決されたと判断した場合、プルリクエスト(GitHub)またはマージリクエスト(GitLab)を開きます
|
||||
3. 実行されたタスクの概要とPR/MRへのリンクを含むコメントを課題に残します
|
||||
|
||||

|
||||
|
||||
#### 課題用のコマンド例
|
||||
|
||||
以下は、課題リゾルバーで使用できるコマンドの例です:
|
||||
|
||||
```
|
||||
@openhands 課題の説明を読んで修正してください
|
||||
```
|
||||
|
||||
### プルリクエスト/マージリクエストの操作
|
||||
|
||||
プルリクエスト(GitHub)またはマージリクエスト(GitLab)でOpenHandsを動作させるには、コメントで`@openhands`を言及して以下を行います:
|
||||
@@ -30,3 +42,15 @@ OpenHands Cloudリポジトリアクセスを許可した後、リポジトリ
|
||||
OpenHandsは以下を行います:
|
||||
1. 作業中であることを知らせるコメントをします
|
||||
2. リクエストされたタスクを実行します
|
||||
|
||||
#### プルリクエスト/マージリクエスト用のコマンド例
|
||||
|
||||
以下は、プルリクエスト/マージリクエストで使用できるコマンドの例です:
|
||||
|
||||
```
|
||||
@openhands レビューコメントを反映してください
|
||||
```
|
||||
|
||||
```
|
||||
@openhands マージの競合を修正し、CIが通過することを確認してください
|
||||
```
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
- ユーザーのGitHub権限(所有者/コラボレーター)
|
||||
3. `インストール&承認`をクリックします
|
||||
|
||||

|
||||
|
||||
## リポジトリアクセスの変更
|
||||
|
||||
リポジトリアクセスはいつでも変更できます:
|
||||
|
||||
+25
-1
@@ -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)
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
+2
@@ -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`
|
||||
|
||||

|
||||
|
||||
## Modificando o Acesso ao Repositório
|
||||
|
||||
Você pode modificar o acesso ao repositório a qualquer momento:
|
||||
|
||||
+25
-1
@@ -8,18 +8,30 @@
|
||||
- [GitHub仓库访问](./github-installation#adding-repository-access)
|
||||
- [GitLab仓库访问](./gitlab-installation#adding-repository-access)
|
||||
|
||||

|
||||
|
||||
## 使用方法
|
||||
|
||||
授予OpenHands Cloud仓库访问权限后,您可以在仓库的问题和拉取/合并请求中使用云问题解决器。
|
||||
|
||||
### 处理问题
|
||||
|
||||
在您的仓库中,给问题添加`openhands`标签。OpenHands将会:
|
||||
在您的仓库中,给问题添加`openhands`标签或添加以`@openhands`开头的消息。OpenHands将会:
|
||||
1. 在问题上发表评论,让您知道它正在处理
|
||||
- 您可以点击链接在OpenHands Cloud上跟踪进度
|
||||
2. 如果确定问题已成功解决,则打开拉取请求(GitHub)或合并请求(GitLab)
|
||||
3. 在问题上发表评论,总结已执行的任务并提供PR/MR的链接
|
||||
|
||||

|
||||
|
||||
#### 问题命令示例
|
||||
|
||||
以下是您可以在问题解决器中使用的命令示例:
|
||||
|
||||
```
|
||||
@openhands 阅读问题描述并修复它
|
||||
```
|
||||
|
||||
### 处理拉取/合并请求
|
||||
|
||||
要让OpenHands处理拉取请求(GitHub)或合并请求(GitLab),在评论中提及`@openhands`以:
|
||||
@@ -30,3 +42,15 @@
|
||||
OpenHands将会:
|
||||
1. 发表评论让您知道它正在处理
|
||||
2. 执行请求的任务
|
||||
|
||||
#### 拉取/合并请求命令示例
|
||||
|
||||
以下是您可以在拉取/合并请求中使用的命令示例:
|
||||
|
||||
```
|
||||
@openhands 反映审查评论
|
||||
```
|
||||
|
||||
```
|
||||
@openhands 修复合并冲突并确保CI通过
|
||||
```
|
||||
|
||||
+2
@@ -36,6 +36,8 @@
|
||||
- 用户的GitHub权限(所有者/协作者)
|
||||
3. 点击`安装并授权`
|
||||
|
||||

|
||||
|
||||
## 修改仓库访问权限
|
||||
|
||||
您可以随时修改仓库访问权限:
|
||||
|
||||
@@ -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,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)
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
#### 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`
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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: `[".*"]`
|
||||
|
||||
Generated
+1687
-1907
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -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",
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
+15
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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": "Не вдалося зберегти електронну пошту"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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)}`;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -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 }}
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user