Skip to content

Commit

Permalink
Merge branch 'main' into add-vscode-hello-world
Browse files Browse the repository at this point in the history
  • Loading branch information
enyst authored Jan 27, 2025
2 parents 30ffec7 + 89c7bf5 commit c16a133
Show file tree
Hide file tree
Showing 29 changed files with 797 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ModelSelector } from "./model-selector";
import { RuntimeSizeSelector } from "./runtime-size-selector";
import { useConfig } from "#/hooks/query/use-config";
import { useCurrentSettings } from "#/context/settings-context";
import { MEMORY_CONDENSER } from "#/utils/feature-flags";

interface SettingsFormProps {
disabled?: boolean;
Expand Down Expand Up @@ -64,12 +65,14 @@ export function SettingsForm({
const isUsingConfirmationMode = !!settings.CONFIRMATION_MODE;
const isUsingBaseUrl = !!settings.LLM_BASE_URL;
const isUsingCustomModel = !!settings.LLM_MODEL && !isKnownModel;
const isUsingDefaultCondenser = !!settings.ENABLE_DEFAULT_CONDENSER;

return (
isUsingSecurityAnalyzer ||
isUsingConfirmationMode ||
isUsingBaseUrl ||
isUsingCustomModel
isUsingCustomModel ||
isUsingDefaultCondenser
);
}

Expand All @@ -94,6 +97,9 @@ export function SettingsForm({
const isUsingAdvancedOptions = keys.includes("use-advanced-options");
const newSettings = extractSettings(formData);

// Inject the condenser config from the current feature flag value
newSettings.ENABLE_DEFAULT_CONDENSER = MEMORY_CONDENSER;

saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic");
await saveUserSettings(newSettings);
onClose();
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/mutation/use-save-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const saveSettingsMutationFn = async (settings: Partial<Settings>) => {
confirmation_mode: settings.CONFIRMATION_MODE,
security_analyzer: settings.SECURITY_ANALYZER,
llm_api_key: settings.LLM_API_KEY?.trim() || undefined,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
};

await OpenHands.saveSettings(apiSettings);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/query/use-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const getSettingsQueryFn = async () => {
LLM_API_KEY: apiSettings.llm_api_key,
REMOTE_RUNTIME_RESOURCE_FACTOR:
apiSettings.remote_runtime_resource_factor,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
};
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4506,5 +4506,8 @@
"fr": "Que voulez-vous construire ?",
"tr": "Ne inşa etmek istiyorsun?",
"de": "Was möchten Sie erstellen?"
},
"SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": {
"en": "Enable Memory Condenser"
}
}
7 changes: 7 additions & 0 deletions frontend/src/services/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Settings = {
CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string;
REMOTE_RUNTIME_RESOURCE_FACTOR: number;
ENABLE_DEFAULT_CONDENSER: boolean;
};

export type ApiSettings = {
Expand All @@ -20,6 +21,7 @@ export type ApiSettings = {
confirmation_mode: boolean;
security_analyzer: string;
remote_runtime_resource_factor: number;
enable_default_condenser: boolean;
};

export const DEFAULT_SETTINGS: Settings = {
Expand All @@ -31,6 +33,7 @@ export const DEFAULT_SETTINGS: Settings = {
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
ENABLE_DEFAULT_CONDENSER: false,
};

export const getCurrentSettingsVersion = () => {
Expand Down Expand Up @@ -60,6 +63,8 @@ export const getLocalStorageSettings = (): Settings => {
const llmApiKey = localStorage.getItem("LLM_API_KEY");
const confirmationMode = localStorage.getItem("CONFIRMATION_MODE") === "true";
const securityAnalyzer = localStorage.getItem("SECURITY_ANALYZER");
const enableDefaultCondenser =
localStorage.getItem("ENABLE_DEFAULT_CONDENSER") === "true";

return {
LLM_MODEL: llmModel || DEFAULT_SETTINGS.LLM_MODEL,
Expand All @@ -71,6 +76,8 @@ export const getLocalStorageSettings = (): Settings => {
SECURITY_ANALYZER: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
REMOTE_RUNTIME_RESOURCE_FACTOR:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
ENABLE_DEFAULT_CONDENSER:
enableDefaultCondenser || DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
};
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ function loadFeatureFlag(
}

export const MULTI_CONVERSATION_UI = loadFeatureFlag("MULTI_CONVERSATION_UI");
export const MEMORY_CONDENSER = loadFeatureFlag("MEMORY_CONDENSER");
44 changes: 18 additions & 26 deletions openhands/agenthub/codeact_agent/codeact_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,33 +433,14 @@ def _get_messages(self, state: State) -> list[Message]:
],
)
]
example_message = self.prompt_manager.get_example_user_message()
if example_message:
messages.append(
Message(
role='user',
content=[TextContent(text=example_message)],
cache_prompt=self.llm.is_caching_prompt_active(),
)
)

# Repository and runtime info
additional_info = self.prompt_manager.get_additional_info()
if self.config.enable_prompt_extensions and additional_info:
# only add these if prompt extension is enabled
messages.append(
Message(
role='user',
content=[TextContent(text=additional_info)],
)
)

pending_tool_call_action_messages: dict[str, Message] = {}
tool_call_id_to_message: dict[str, Message] = {}

# Condense the events from the state.
events = self.condenser.condensed_history(state)

is_first_message_handled = False
for event in events:
# create a regular message from an event
if isinstance(event, Action):
Expand Down Expand Up @@ -501,19 +482,30 @@ def _get_messages(self, state: State) -> list[Message]:
for response_id in _response_ids_to_remove:
pending_tool_call_action_messages.pop(response_id)

for message in messages_to_add:
if message:
if message.role == 'user':
self.prompt_manager.enhance_message(message)
messages.append(message)
for msg in messages_to_add:
if msg:
if msg.role == 'user' and not is_first_message_handled:
is_first_message_handled = True
# compose the first user message with examples
self.prompt_manager.add_examples_to_initial_message(msg)

# and/or repo/runtime info
if self.config.enable_prompt_extensions:
self.prompt_manager.add_info_to_initial_message(msg)

# enhance the user message with additional context based on keywords matched
if msg.role == 'user':
self.prompt_manager.enhance_message(msg)

messages.append(msg)

if self.llm.is_caching_prompt_active():
# NOTE: this is only needed for anthropic
# following logic here:
# https://github.com/anthropics/anthropic-quickstarts/blob/8f734fd08c425c6ec91ddd613af04ff87d70c5a0/computer-use-demo/computer_use_demo/loop.py#L241-L262
breakpoints_remaining = 3 # remaining 1 for system/tool
for message in reversed(messages):
if message.role == 'user' or message.role == 'tool':
if message.role in ('user', 'tool'):
if breakpoints_remaining > 0:
message.content[
-1
Expand Down
53 changes: 45 additions & 8 deletions openhands/agenthub/codeact_agent/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
ModelResponse,
)

from openhands.core.exceptions import FunctionCallNotExistsError
from openhands.core.exceptions import (
FunctionCallNotExistsError,
FunctionCallValidationError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
Expand Down Expand Up @@ -494,15 +497,19 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
) from e
if tool_call.function.name == 'execute_bash':
# this is an LLM error: add empty command to avoid breaking the tool call
if 'command' not in arguments:
arguments['command'] = ''
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
)
# convert is_input to boolean
if 'is_input' in arguments:
arguments['is_input'] = arguments['is_input'] == 'true'
action = CmdRunAction(**arguments)
is_input = arguments.get('is_input', 'false') == 'true'
action = CmdRunAction(command=arguments['command'], is_input=is_input)
elif tool_call.function.name == 'execute_ipython_cell':
action = IPythonRunCellAction(**arguments)
if 'code' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "code" in tool call {tool_call.function.name}'
)
action = IPythonRunCellAction(code=arguments['code'])
elif tool_call.function.name == 'delegate_to_browsing_agent':
action = AgentDelegateAction(
agent='BrowsingAgent',
Expand All @@ -511,8 +518,30 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
elif tool_call.function.name == 'finish':
action = AgentFinishAction()
elif tool_call.function.name == 'edit_file':
action = FileEditAction(**arguments)
if 'path' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "path" in tool call {tool_call.function.name}'
)
if 'content' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "content" in tool call {tool_call.function.name}'
)
action = FileEditAction(
path=arguments['path'],
content=arguments['content'],
start=arguments.get('start', 1),
end=arguments.get('end', -1),
)
elif tool_call.function.name == 'str_replace_editor':
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
)
if 'path' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "path" in tool call {tool_call.function.name}'
)

# We implement this in agent_skills, which can be used via Jupyter
# convert tool_call.function.arguments to kwargs that can be passed to file_editor
code = f'print(file_editor(**{arguments}))'
Expand All @@ -534,8 +563,16 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
impl_source=FileEditSource.OH_ACI,
)
elif tool_call.function.name == 'browser':
if 'code' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "code" in tool call {tool_call.function.name}'
)
action = BrowseInteractiveAction(browser_actions=arguments['code'])
elif tool_call.function.name == 'web_read':
if 'url' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])
else:
raise FunctionCallNotExistsError(
Expand Down
2 changes: 1 addition & 1 deletion openhands/core/config/sandbox_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SandboxConfig(BaseModel):
This should be a JSON string that will be parsed into a dictionary.
"""

remote_runtime_api_url: str = Field(default='http://localhost:8000')
remote_runtime_api_url: str | None = Field(default='http://localhost:8000')
local_runtime_url: str = Field(default='http://localhost')
keep_runtime_alive: bool = Field(default=False)
rm_all_containers: bool = Field(default=False)
Expand Down
10 changes: 5 additions & 5 deletions openhands/events/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def _should_filter_event(
self,
event,
query: str | None = None,
event_type: str | None = None,
event_type: type[Event] | None = None,
source: str | None = None,
start_date: str | None = None,
end_date: str | None = None,
Expand All @@ -329,15 +329,15 @@ def _should_filter_event(
Args:
event: The event to check
query (str, optional): Text to search for in event content
event_type (str, optional): Filter by event type (e.g., "FileReadAction")
event_type (type[Event], optional): Filter by event type class (e.g., FileReadAction)
source (str, optional): Filter by event source
start_date (str, optional): Filter events after this date (ISO format)
end_date (str, optional): Filter events before this date (ISO format)
Returns:
bool: True if the event should be filtered out, False if it matches all criteria
"""
if event_type and not event.__class__.__name__ == event_type:
if event_type and not isinstance(event, event_type):
return True

if source and not event.source.value == source:
Expand All @@ -361,7 +361,7 @@ def _should_filter_event(
def get_matching_events(
self,
query: str | None = None,
event_type: str | None = None,
event_type: type[Event] | None = None,
source: str | None = None,
start_date: str | None = None,
end_date: str | None = None,
Expand All @@ -372,7 +372,7 @@ def get_matching_events(
Args:
query (str, optional): Text to search for in event content
event_type (str, optional): Filter by event type (e.g., "FileReadAction")
event_type (type[Event], optional): Filter by event type class (e.g., FileReadAction)
source (str, optional): Filter by event source
start_date (str, optional): Filter events after this date (ISO format)
end_date (str, optional): Filter events before this date (ISO format)
Expand Down
1 change: 0 additions & 1 deletion openhands/llm/fn_call_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ def index():
Running the updated file:
<function=execute_bash>
<parameter=command>
<parameter=command>
python3 app.py > server.log 2>&1 &
</parameter>
</function>
Expand Down
20 changes: 8 additions & 12 deletions openhands/runtime/impl/docker/docker_runtime.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import atexit
from functools import lru_cache
from typing import Callable
from uuid import UUID

import docker
import requests
Expand All @@ -26,6 +26,7 @@
from openhands.runtime.utils.log_streamer import LogStreamer
from openhands.runtime.utils.runtime_build import build_runtime_image
from openhands.utils.async_utils import call_sync_from_async
from openhands.utils.shutdown_listener import add_shutdown_listener
from openhands.utils.tenacity_stop import stop_if_should_exit

CONTAINER_NAME_PREFIX = 'openhands-runtime-'
Expand All @@ -36,13 +37,6 @@
APP_PORT_RANGE_2 = (55000, 59999)


def stop_all_runtime_containers():
stop_all_containers(CONTAINER_NAME_PREFIX)


_atexit_registered = False


class DockerRuntime(ActionExecutionClient):
"""This runtime will subscribe the event stream.
When receive an event, it will send the event to runtime-client which run inside the docker environment.
Expand All @@ -55,6 +49,8 @@ class DockerRuntime(ActionExecutionClient):
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
"""

_shutdown_listener_id: UUID | None = None

def __init__(
self,
config: AppConfig,
Expand All @@ -66,10 +62,10 @@ def __init__(
attach_to_existing: bool = False,
headless_mode: bool = True,
):
global _atexit_registered
if not _atexit_registered:
_atexit_registered = True
atexit.register(stop_all_runtime_containers)
if not DockerRuntime._shutdown_listener_id:
DockerRuntime._shutdown_listener_id = add_shutdown_listener(
lambda: stop_all_containers(CONTAINER_NAME_PREFIX)
)

self.config = config
self._runtime_initialized: bool = False
Expand Down
4 changes: 4 additions & 0 deletions openhands/runtime/impl/remote/remote_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def __init__(
'debug',
'Setting workspace_base is not supported in the remote runtime.',
)
if self.config.sandbox.remote_runtime_api_url is None:
raise ValueError(
'remote_runtime_api_url is required in the remote runtime.'
)

self.runtime_builder = RemoteRuntimeBuilder(
self.config.sandbox.remote_runtime_api_url,
Expand Down
Loading

0 comments on commit c16a133

Please sign in to comment.