Compare commits

..

2 Commits

Author SHA1 Message Date
Robert Brennan 04112e4a24 outline refactor 2025-05-27 21:56:24 -04:00
openhands 7b19ee0d5e Add documentation for using OpenHands as a library 2025-05-28 01:06:57 +00:00
10 changed files with 530 additions and 49 deletions
-1
View File
@@ -26,7 +26,6 @@ Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To run all the unit tests, run `poetry run pytest --forked -n auto -svv ./tests/unit`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
+19
View File
@@ -0,0 +1,19 @@
[ ] 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.
+84
View File
@@ -0,0 +1,84 @@
# Using OpenHands as a library
## Hello World
```python
import asyncio
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
from openhands.core.setup import run_agent
async def run_openhands_agent():
final_state = await run_agent(
config=OpenHandsConfig(
llm=LLMConfig(
model="claude-sonnet-4-20250514",
api_key="your_api_key_here", # Replace with your actual API key
),
),
initial_user_message="Flip a coin",
context_message="You build simple programs and run them.",
)
return final_state
# Run the async function
if __name__ == "__main__":
final_state = asyncio.run(run_openhands_agent())
print("Agent execution completed!")
```
## Using the internals
```python
import asyncio
from openhands.controller.agent import Agent
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
from openhands.events.action import MessageAction
from openhands.llm.llm import LLM
from openhands.core.setup import (
create_runtime,
create_memory,
generate_sid,
)
from openhands.core.main import run_controller
async def run_openhands_agent():
config = OpenHandsConfig(
runtime="local",
file_store="memory",
llm=LLMConfig(
model="claude-sonnet-4-20250514", # Choose your preferred model
api_key="your_api_key_here", # Replace with your actual API key
temperature=0.0, # Set temperature to 0 for deterministic output
),
agent=AgentConfig(
enable_browsing=False,
),
)
oh = OpenHands(config=config)
conversation = oh.create_conversation(
conversation_id='hello-world',
)
await conversation.runtime.connect()
def on_event(event: Event) -> None:
print(f"Event received: {event}")
conversation.event_stream.subscribe(EventStreamSubscriber.MAIN, on_event)
initial_user_action = MessageAction(content="Flip a coin")
conversation.event_stream.add_event(initial_user_action, EventSource.USER)
while conversation.state.agent_state not in end_states:
await asyncio.sleep(1)
await runtime.close()
return conversation.state
# Run the async function
if __name__ == "__main__":
final_state = asyncio.run(run_openhands_agent())
print("Agent execution completed!")
```
+1 -1
View File
@@ -1,5 +1,5 @@
{
"items": ["python/python"],
"items": ["python/python", "python/using-openhands-as-library"],
"label": "Backend",
"type": "category"
}
@@ -0,0 +1,399 @@
# 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.
@@ -26,6 +26,7 @@ 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";
@@ -180,7 +181,11 @@ export function ChatInterface() {
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
</div>
{errorMessage && <ErrorMessageBanner message={errorMessage} />}
{errorMessage && (
<ErrorMessageBanner
message={i18n.exists(errorMessage) ? t(errorMessage) : errorMessage}
/>
)}
<InteractiveChatBox
onSubmit={handleSendMessage}
@@ -1,7 +1,3 @@
import { Trans } from "react-i18next";
import { Link } from "react-router";
import i18n from "#/i18n";
interface ErrorMessageBannerProps {
message: string;
}
@@ -9,23 +5,7 @@ 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">
{i18n.exists(message) ? (
<Trans
i18nKey={message}
components={{
a: (
<Link
className="underline font-bold cursor-pointer"
to="/settings/billing"
>
link
</Link>
),
}}
/>
) : (
message
)}
{message}
</div>
);
}
+3 -8
View File
@@ -217,14 +217,9 @@ export function WsClientProvider({
isFileWriteAction(event) ||
isCommandAction(event)
) {
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 },
);
queryClient.removeQueries({
queryKey: ["file_changes", conversationId],
});
// Invalidate file diff cache when a file is edited or written
if (!isCommandAction(event)) {
+2 -2
View File
@@ -228,6 +228,8 @@ 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",
@@ -548,6 +550,4 @@ 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",
}
+15 -15
View File
@@ -6400,20 +6400,20 @@
"uk": "Запит не вдалося виконати через внутрішню помилку сервера."
},
"STATUS$ERROR_LLM_OUT_OF_CREDITS": {
"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>"
"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"
},
"STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION": {
"en": "Content policy violation. The output was blocked by content filtering policy.",
@@ -8780,7 +8780,7 @@
"ar": "إرسال...",
"fr": "Envoi...",
"tr": "Gönderiliyor...",
"de": "Senden...",
"de": "Senden...",
"uk": "Відправляємо..."
},
"FEEDBACK$SUBMITTING_MESSAGE": {