From 89963e93d80d2a9b0d5253d4b260b612be62735b Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Tue, 21 Jan 2025 04:22:48 +0100 Subject: [PATCH 01/18] Re-add reasoning effort (#6371) --- openhands/core/config/llm_config.py | 7 ++++--- openhands/llm/llm.py | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openhands/core/config/llm_config.py b/openhands/core/config/llm_config.py index c28390d47bda..9beb6d6f5f09 100644 --- a/openhands/core/config/llm_config.py +++ b/openhands/core/config/llm_config.py @@ -1,8 +1,8 @@ from __future__ import annotations import os - from typing import Any + from pydantic import BaseModel, Field, SecretStr from openhands.core.logger import LOG_DIR @@ -39,12 +39,12 @@ class LLMConfig(BaseModel): drop_params: Drop any unmapped (unsupported) params without causing an exception. modify_params: Modify params allows litellm to do transformations like adding a default message, when a message is empty. disable_vision: If model is vision capable, this option allows to disable image processing (useful for cost reduction). - reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models. caching_prompt: Use the prompt caching feature if provided by the LLM and supported by the provider. log_completions: Whether to log LLM completions to the state. log_completions_folder: The folder to log LLM completions to. Required if log_completions is True. custom_tokenizer: A custom tokenizer to use for token counting. native_tool_calling: Whether to use native tool calling if supported by the model. Can be True, False, or not set. + reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models. """ model: str = Field(default='claude-3-5-sonnet-20241022') @@ -85,7 +85,8 @@ class LLMConfig(BaseModel): log_completions_folder: str = Field(default=os.path.join(LOG_DIR, 'completions')) custom_tokenizer: str | None = Field(default=None) native_tool_calling: bool | None = Field(default=None) - + reasoning_effort: str | None = Field(default=None) + model_config = {'extra': 'forbid'} def model_post_init(self, __context: Any): diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index 8f9ac12b7063..98bcf7cb173d 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -152,6 +152,12 @@ def __init__( temperature=self.config.temperature, top_p=self.config.top_p, drop_params=self.config.drop_params, + # add reasoning_effort, only if the model is supported + **( + {'reasoning_effort': self.config.reasoning_effort} + if self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS + else {} + ), ) self._completion_unwrapped = self._completion @@ -217,10 +223,6 @@ def wrapper(*args, **kwargs): 'anthropic-beta': 'prompt-caching-2024-07-31', } - # Set reasoning effort for models that support it - if self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS: - kwargs['reasoning_effort'] = self.config.reasoning_effort - # set litellm modify_params to the configured value # True by default to allow litellm to do transformations like adding a default message, when a message is empty # NOTE: this setting is global; unlike drop_params, it cannot be overridden in the litellm completion partial From 509892cf0e41cee92447e4960ba7810f3fb8dda1 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Mon, 20 Jan 2025 22:23:21 -0500 Subject: [PATCH 02/18] Revert changes to config defaults (#6370) --- openhands/core/config/agent_config.py | 2 +- openhands/core/config/sandbox_config.py | 2 +- openhands/utils/prompt.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openhands/core/config/agent_config.py b/openhands/core/config/agent_config.py index 43e430b879da..67fa4e9d8a5d 100644 --- a/openhands/core/config/agent_config.py +++ b/openhands/core/config/agent_config.py @@ -27,6 +27,6 @@ class AgentConfig(BaseModel): memory_enabled: bool = Field(default=False) memory_max_threads: int = Field(default=3) llm_config: str | None = Field(default=None) - enable_prompt_extensions: bool = Field(default=False) + enable_prompt_extensions: bool = Field(default=True) disabled_microagents: list[str] | None = Field(default=None) condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig) diff --git a/openhands/core/config/sandbox_config.py b/openhands/core/config/sandbox_config.py index 44bd7ee0afda..f5b984fec0b9 100644 --- a/openhands/core/config/sandbox_config.py +++ b/openhands/core/config/sandbox_config.py @@ -60,7 +60,7 @@ class SandboxConfig(BaseModel): runtime_startup_env_vars: dict[str, str] = Field(default_factory=dict) browsergym_eval_env: str | None = Field(default=None) platform: str | None = Field(default=None) - close_delay: int = Field(default=900) + close_delay: int = Field(default=15) remote_runtime_resource_factor: int = Field(default=1) enable_gpu: bool = Field(default=False) docker_runtime_kwargs: str | None = Field(default=None) diff --git a/openhands/utils/prompt.py b/openhands/utils/prompt.py index 1861c45308b5..1ffd4b8f117b 100644 --- a/openhands/utils/prompt.py +++ b/openhands/utils/prompt.py @@ -48,7 +48,8 @@ class RepositoryInfo: * {{ host }} (port {{ port }}) {% endfor %} When starting a web server, use the corresponding ports. You should also -set any options to allow iframes and CORS requests. +set any options to allow iframes and CORS requests, and allow the server to +be accessed from any host (e.g. 0.0.0.0). {% endif %} """ From 210eeee94a8ac2a9b6f7a0c8563eef1f2fda36d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:46:56 +0400 Subject: [PATCH 03/18] chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6358) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 18 ++++++++++-------- frontend/package.json | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 70068c558317..67bf890a9301 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,7 +52,7 @@ "@playwright/test": "^1.49.1", "@react-router/dev": "^7.1.2", "@tailwindcss/typography": "^0.5.16", - "@tanstack/eslint-plugin-query": "^5.62.16", + "@tanstack/eslint-plugin-query": "^5.64.2", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.0", @@ -73,7 +73,7 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.2.2", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^4.6.2", "husky": "^9.1.6", @@ -5461,10 +5461,11 @@ } }, "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.62.16", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.16.tgz", - "integrity": "sha512-VhnHSQ/hc62olLzGhlLJ4BJGWynwjs3cDMsByasKJ3zjW1YZ+6raxOv0gHHISm+VEnAY42pkMowmSWrXfL4NTw==", + "version": "5.64.2", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.64.2.tgz", + "integrity": "sha512-Xq7jRYvNtGMHjQEGUZLHgEMNB59hgTlqdmKor6cdJ6CMZ/nwmBGpnlr/dcHden7W7BPCdBVN4PWMZBICWvCNQQ==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/utils": "^8.18.1" }, @@ -8739,10 +8740,11 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz", - "integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" diff --git a/frontend/package.json b/frontend/package.json index e9c61b2d8432..dbfa19c338ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -79,7 +79,7 @@ "@playwright/test": "^1.49.1", "@react-router/dev": "^7.1.2", "@tailwindcss/typography": "^0.5.16", - "@tanstack/eslint-plugin-query": "^5.62.16", + "@tanstack/eslint-plugin-query": "^5.64.2", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.0", @@ -100,7 +100,7 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.2.2", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^4.6.2", "husky": "^9.1.6", From b7f34c3f8dbd37eb623778d0e3bafe4480adbbf5 Mon Sep 17 00:00:00 2001 From: Boxuan Li Date: Tue, 21 Jan 2025 06:10:00 -0800 Subject: [PATCH 04/18] (feat) Add button to export trajectory on chat panel (#6378) --- frontend/src/api/open-hands.ts | 10 +++++ frontend/src/api/open-hands.types.ts | 5 +++ .../features/chat/chat-interface.tsx | 29 ++++++++++++++ .../features/export/export-actions.tsx | 17 ++++++++ .../shared/buttons/export-action-button.tsx | 17 ++++++++ .../src/hooks/mutation/use-get-trajectory.ts | 7 ++++ frontend/src/icons/export.svg | 5 +++ frontend/src/types/file-system.d.ts | 12 ++++++ frontend/src/utils/download-files.ts | 40 +++++++++++++++++++ openhands/server/app.py | 2 + openhands/server/routes/trajectory.py | 40 +++++++++++++++++++ 11 files changed, 184 insertions(+) create mode 100644 frontend/src/components/features/export/export-actions.tsx create mode 100644 frontend/src/components/shared/buttons/export-action-button.tsx create mode 100644 frontend/src/hooks/mutation/use-get-trajectory.ts create mode 100644 frontend/src/icons/export.svg create mode 100644 openhands/server/routes/trajectory.py diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index bcb183a106a7..caef81a9a1a1 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -10,6 +10,7 @@ import { AuthenticateResponse, Conversation, ResultSet, + GetTrajectoryResponse, } from "./open-hands.types"; import { openHands } from "./open-hands-axios"; import { ApiSettings } from "#/services/settings"; @@ -354,6 +355,15 @@ class OpenHands { return response.data.items; } + + static async getTrajectory( + conversationId: string, + ): Promise { + const { data } = await openHands.get( + `/api/conversations/${conversationId}/trajectory`, + ); + return data; + } } export default OpenHands; diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index 169de47afb39..995c2f5f7203 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -55,6 +55,11 @@ export interface GetVSCodeUrlResponse { error?: string; } +export interface GetTrajectoryResponse { + trajectory: unknown[] | null; + error?: string; +} + export interface AuthenticateResponse { message?: string; error?: string; diff --git a/frontend/src/components/features/chat/chat-interface.tsx b/frontend/src/components/features/chat/chat-interface.tsx index eb934b69b9ed..e04efa19b3cc 100644 --- a/frontend/src/components/features/chat/chat-interface.tsx +++ b/frontend/src/components/features/chat/chat-interface.tsx @@ -1,8 +1,11 @@ import { useDispatch, useSelector } from "react-redux"; +import toast from "react-hot-toast"; import React from "react"; import posthog from "posthog-js"; +import { useParams } from "react-router"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; import { FeedbackActions } from "../feedback/feedback-actions"; +import { ExportActions } from "../export/export-actions"; import { createChatMessage } from "#/services/chat-service"; import { InteractiveChatBox } from "./interactive-chat-box"; import { addUserMessage } from "#/state/chat-slice"; @@ -19,6 +22,8 @@ import { ActionSuggestions } from "./action-suggestions"; import { ContinueButton } from "#/components/shared/buttons/continue-button"; import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button"; import { LoadingSpinner } from "#/components/shared/loading-spinner"; +import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory"; +import { downloadTrajectory } from "#/utils/download-files"; function getEntryPoint( hasRepository: boolean | null, @@ -47,6 +52,8 @@ export function ChatInterface() { const { selectedRepository, importedProjectZip } = useSelector( (state: RootState) => state.initialQuery, ); + const params = useParams(); + const { mutate: getTrajectory } = useGetTrajectory(); const handleSendMessage = async (content: string, files: File[]) => { if (messages.length === 0) { @@ -90,6 +97,25 @@ export function ChatInterface() { setFeedbackPolarity(polarity); }; + const onClickExportTrajectoryButton = () => { + if (!params.conversationId) { + toast.error("ConversationId unknown, cannot download trajectory"); + return; + } + + getTrajectory(params.conversationId, { + onSuccess: async (data) => { + await downloadTrajectory( + params.conversationId ?? "unknown", + data.trajectory, + ); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + }; + const isWaitingForUserInput = curAgentState === AgentState.AWAITING_USER_INPUT || curAgentState === AgentState.FINISHED; @@ -137,6 +163,9 @@ export function ChatInterface() { onClickShareFeedbackActionButton("negative") } /> + onClickExportTrajectoryButton()} + />
{messages.length > 2 && diff --git a/frontend/src/components/features/export/export-actions.tsx b/frontend/src/components/features/export/export-actions.tsx new file mode 100644 index 000000000000..faafd98af293 --- /dev/null +++ b/frontend/src/components/features/export/export-actions.tsx @@ -0,0 +1,17 @@ +import ExportIcon from "#/icons/export.svg?react"; +import { ExportActionButton } from "#/components/shared/buttons/export-action-button"; + +interface ExportActionsProps { + onExportTrajectory: () => void; +} + +export function ExportActions({ onExportTrajectory }: ExportActionsProps) { + return ( +
+ } + /> +
+ ); +} diff --git a/frontend/src/components/shared/buttons/export-action-button.tsx b/frontend/src/components/shared/buttons/export-action-button.tsx new file mode 100644 index 000000000000..b76443591d39 --- /dev/null +++ b/frontend/src/components/shared/buttons/export-action-button.tsx @@ -0,0 +1,17 @@ +interface ExportActionButtonProps { + onClick: () => void; + icon: React.ReactNode; +} + +export function ExportActionButton({ onClick, icon }: ExportActionButtonProps) { + return ( + + ); +} diff --git a/frontend/src/hooks/mutation/use-get-trajectory.ts b/frontend/src/hooks/mutation/use-get-trajectory.ts new file mode 100644 index 000000000000..e2ad96e64d24 --- /dev/null +++ b/frontend/src/hooks/mutation/use-get-trajectory.ts @@ -0,0 +1,7 @@ +import { useMutation } from "@tanstack/react-query"; +import OpenHands from "#/api/open-hands"; + +export const useGetTrajectory = () => + useMutation({ + mutationFn: (cid: string) => OpenHands.getTrajectory(cid), + }); diff --git a/frontend/src/icons/export.svg b/frontend/src/icons/export.svg new file mode 100644 index 000000000000..d9d52ecb48ac --- /dev/null +++ b/frontend/src/icons/export.svg @@ -0,0 +1,5 @@ + + + diff --git a/frontend/src/types/file-system.d.ts b/frontend/src/types/file-system.d.ts index c2d823f701e0..90caf2ba3809 100644 --- a/frontend/src/types/file-system.d.ts +++ b/frontend/src/types/file-system.d.ts @@ -26,6 +26,18 @@ interface FileSystemDirectoryHandle { ): Promise; } +interface SaveFilePickerOptions { + suggestedName?: string; + types?: Array<{ + description?: string; + accept: Record; + }>; + excludeAcceptAllOption?: boolean; +} + interface Window { showDirectoryPicker(): Promise; + showSaveFilePicker( + options?: SaveFilePickerOptions, + ): Promise; } diff --git a/frontend/src/utils/download-files.ts b/frontend/src/utils/download-files.ts index 1bcd5eb0fd8f..5e17c2b5c317 100644 --- a/frontend/src/utils/download-files.ts +++ b/frontend/src/utils/download-files.ts @@ -22,6 +22,13 @@ function isFileSystemAccessSupported(): boolean { return "showDirectoryPicker" in window; } +/** + * Checks if the Save File Picker API is supported + */ +function isSaveFilePickerSupported(): boolean { + return "showSaveFilePicker" in window; +} + /** * Creates subdirectories and returns the final directory handle */ @@ -162,6 +169,39 @@ async function processBatch( }; } +export async function downloadTrajectory( + conversationId: string, + data: unknown[] | null, +): Promise { + try { + if (!isSaveFilePickerSupported()) { + throw new Error( + "Your browser doesn't support downloading folders. Please use Chrome, Edge, or another browser that supports the File System Access API.", + ); + } + const options = { + suggestedName: `trajectory-${conversationId}.json`, + types: [ + { + description: "JSON File", + accept: { + "application/json": [".json"], + }, + }, + ], + }; + + const handle = await window.showSaveFilePicker(options); + const writable = await handle.createWritable(); + await writable.write(JSON.stringify(data, null, 2)); + await writable.close(); + } catch (error) { + throw new Error( + `Failed to download file: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + /** * Downloads files from the workspace one by one * @param initialPath Initial path to start downloading from. If not provided, downloads from root diff --git a/openhands/server/app.py b/openhands/server/app.py index ea95bcf3e0b4..4e1a094317da 100644 --- a/openhands/server/app.py +++ b/openhands/server/app.py @@ -27,6 +27,7 @@ from openhands.server.routes.public import app as public_api_router from openhands.server.routes.security import app as security_api_router from openhands.server.routes.settings import app as settings_router +from openhands.server.routes.trajectory import app as trajectory_router from openhands.server.shared import openhands_config, session_manager from openhands.utils.import_utils import get_impl @@ -69,6 +70,7 @@ async def health(): app.include_router(manage_conversation_api_router) app.include_router(settings_router) app.include_router(github_api_router) +app.include_router(trajectory_router) AttachConversationMiddlewareImpl = get_impl( AttachConversationMiddleware, openhands_config.attach_conversation_middleware_path diff --git a/openhands/server/routes/trajectory.py b/openhands/server/routes/trajectory.py new file mode 100644 index 000000000000..5b2e0097f284 --- /dev/null +++ b/openhands/server/routes/trajectory.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +from openhands.core.logger import openhands_logger as logger +from openhands.events.serialization import event_to_trajectory +from openhands.events.stream import AsyncEventStreamWrapper + +app = APIRouter(prefix='/api/conversations/{conversation_id}') + + +@app.get('/trajectory') +async def get_trajectory(request: Request): + """Get trajectory. + + This function retrieves the current trajectory and returns it. + + Args: + request (Request): The incoming request object. + + Returns: + JSONResponse: A JSON response containing the trajectory as a list of + events. + """ + try: + async_stream = AsyncEventStreamWrapper( + request.state.conversation.event_stream, filter_hidden=True + ) + trajectory = [] + async for event in async_stream: + trajectory.append(event_to_trajectory(event)) + return JSONResponse(status_code=200, content={'trajectory': trajectory}) + except Exception as e: + logger.error(f'Error getting trajectory: {e}', exc_info=True) + return JSONResponse( + status_code=500, + content={ + 'trajectory': None, + 'error': f'Error getting trajectory: {e}', + }, + ) From 54589d7e8374109c18fc72e0fdb2c982ef4aef0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:10:20 +0000 Subject: [PATCH 05/18] chore(deps-dev): bump pre-commit from 4.0.1 to 4.1.0 in the pre-commit group (#6384) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index c84eed65a096..3087a037f908 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5999,13 +5999,13 @@ test = ["coverage", "django", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)" [[package]] name = "pre-commit" -version = "4.0.1" +version = "4.1.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, - {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, ] [package.dependencies] @@ -9857,4 +9857,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5358d6cd39f8e254c098d3ffcbb2f57213611ad132bf7e2193005f92e45b255c" +content-hash = "f0fdb1fa00337a3fdda425cbfb9af7020d7460fdca8eb9dcfbe4817cf60d0a05" diff --git a/pyproject.toml b/pyproject.toml index ca201f2d4f0c..3a1b4f70d979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ llama-index-embeddings-voyageai = "*" [tool.poetry.group.dev.dependencies] ruff = "0.9.2" mypy = "1.14.1" -pre-commit = "4.0.1" +pre-commit = "4.1.0" build = "*" [tool.poetry.group.test.dependencies] From 7f57dbebda6c3e7c9e0d256a30820e18f1e2f7c0 Mon Sep 17 00:00:00 2001 From: louria Date: Tue, 21 Jan 2025 23:26:47 +0800 Subject: [PATCH 06/18] Update MiniWoB README (#6385) --- evaluation/benchmarks/miniwob/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evaluation/benchmarks/miniwob/README.md b/evaluation/benchmarks/miniwob/README.md index 3809925b3fd6..002489097b94 100644 --- a/evaluation/benchmarks/miniwob/README.md +++ b/evaluation/benchmarks/miniwob/README.md @@ -8,6 +8,9 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve ## Test if your environment works +Follow the instructions here https://miniwob.farama.org/content/getting_started/ & https://miniwob.farama.org/content/viewing/ +to set up MiniWoB server in your local environment at http://localhost:8080/miniwob/ + Access with browser the above MiniWoB URLs and see if they load correctly. ## Run Evaluation From 25fdb0c3bf386bbd93eb77dc4ff269e631eb2d96 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 21 Jan 2025 11:15:28 -0500 Subject: [PATCH 07/18] fix api key value (#6388) --- openhands/utils/embeddings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openhands/utils/embeddings.py b/openhands/utils/embeddings.py index 6791787d3204..9ab66731a97c 100644 --- a/openhands/utils/embeddings.py +++ b/openhands/utils/embeddings.py @@ -100,7 +100,9 @@ def get_embedding_model(strategy: str, llm_config: LLMConfig) -> 'BaseEmbedding' return AzureOpenAIEmbedding( model='text-embedding-ada-002', deployment_name=llm_config.embedding_deployment_name, - api_key=llm_config.api_key, + api_key=llm_config.api_key.get_secret_value() + if llm_config.api_key + else None, azure_endpoint=llm_config.base_url, api_version=llm_config.api_version, ) From 8ae36481dffa5899865e7d02e1274cabb412a4e9 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 21 Jan 2025 12:00:59 -0500 Subject: [PATCH 08/18] Fix API key again (#6390) --- openhands/server/listen_socket.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openhands/server/listen_socket.py b/openhands/server/listen_socket.py index baa79a4c6e35..7e0f93066914 100644 --- a/openhands/server/listen_socket.py +++ b/openhands/server/listen_socket.py @@ -37,7 +37,11 @@ async def connect(connection_id: str, environ, auth): if not signed_token: logger.error('No github_auth cookie') raise ConnectionRefusedError('No github_auth cookie') - decoded = jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256']) + if not config.jwt_secret: + raise RuntimeError('JWT secret not found') + decoded = jwt.decode( + signed_token, config.jwt_secret.get_secret_value(), algorithms=['HS256'] + ) user_id = decoded['github_user_id'] logger.info(f'User {user_id} is connecting to conversation {conversation_id}') From 5b7fcfbe1ad07b9fbb6df995889f86ee29bdebbf Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Tue, 21 Jan 2025 18:18:30 +0100 Subject: [PATCH 09/18] Disable prompt extensions in SWE-bench (#6391) --- .../current/usage/configuration-options.md | 2 +- evaluation/benchmarks/swe_bench/run_infer.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/configuration-options.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/configuration-options.md index 81070c553365..0f22d218b817 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/configuration-options.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/configuration-options.md @@ -373,7 +373,7 @@ Les options de configuration de l'agent sont définies dans les sections `[agent - Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction) **Utilisation du micro-agent** -- `use_microagents` +- `enable_prompt_extensions` - Type : `bool` - Valeur par défaut : `true` - Description : Indique si l'utilisation des micro-agents est activée ou non diff --git a/evaluation/benchmarks/swe_bench/run_infer.py b/evaluation/benchmarks/swe_bench/run_infer.py index 511a22c4ed9a..7d13d546d881 100644 --- a/evaluation/benchmarks/swe_bench/run_infer.py +++ b/evaluation/benchmarks/swe_bench/run_infer.py @@ -158,6 +158,7 @@ def get_config( codeact_enable_browsing=RUN_WITH_BROWSING, codeact_enable_llm_editor=False, condenser=metadata.condenser_config, + enable_prompt_extensions=False, ) config.set_agent_config(agent_config) return config From 8bd761372431f44bdc7e7db9100d8d920366610c Mon Sep 17 00:00:00 2001 From: Calvin Smith Date: Tue, 21 Jan 2025 11:04:30 -0700 Subject: [PATCH 10/18] fix: Settings modal properly tracks if an API key is set (#6394) Co-authored-by: Calvin Smith --- frontend/__tests__/components/features/sidebar/sidebar.test.tsx | 2 +- .../src/components/shared/modals/settings/settings-form.tsx | 2 +- frontend/src/context/settings-context.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx index 14b84a14b628..7b478219e283 100644 --- a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx +++ b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx @@ -156,7 +156,7 @@ describe("Sidebar", () => { await user.click(advancedOptionsSwitch); const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i); - await user.type(apiKeyInput, "SET"); + await user.type(apiKeyInput, "**********"); const saveButton = within(settingsModal).getByTestId( "save-settings-button", diff --git a/frontend/src/components/shared/modals/settings/settings-form.tsx b/frontend/src/components/shared/modals/settings/settings-form.tsx index 9883267c3c1a..e2637bc594d3 100644 --- a/frontend/src/components/shared/modals/settings/settings-form.tsx +++ b/frontend/src/components/shared/modals/settings/settings-form.tsx @@ -171,7 +171,7 @@ export function SettingsForm({ {showAdvancedOptions && ( diff --git a/frontend/src/context/settings-context.tsx b/frontend/src/context/settings-context.tsx index bbbaa3ecbbae..4ecc105f9b7f 100644 --- a/frontend/src/context/settings-context.tsx +++ b/frontend/src/context/settings-context.tsx @@ -34,7 +34,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { ...newSettings, }; - if (updatedSettings.LLM_API_KEY === "SET") { + if (updatedSettings.LLM_API_KEY === "**********") { delete updatedSettings.LLM_API_KEY; } From ff3880c76db75238e9bc3ae4689e46751a87166e Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Tue, 21 Jan 2025 13:13:43 -0500 Subject: [PATCH 11/18] fix(remote_runtime): define runtime_id first to fix attrbute error (#6393) --- openhands/runtime/impl/remote/remote_runtime.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 3deb7ba40814..79dfd3026d30 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -31,6 +31,9 @@ class RemoteRuntime(ActionExecutionClient): """This runtime will connect to a remote oh-runtime-client.""" port: int = 60000 # default port for the remote runtime client + runtime_id: str | None = None + runtime_url: str | None = None + _runtime_initialized: bool = False def __init__( self, @@ -71,10 +74,7 @@ def __init__( self.config.sandbox.api_key, self.session, ) - self.runtime_id: str | None = None - self.runtime_url: str | None = None self.available_hosts: dict[str, int] = {} - self._runtime_initialized: bool = False def log(self, level: str, message: str) -> None: message = f'[runtime session_id={self.sid} runtime_id={self.runtime_id or "unknown"}] {message}' From 09e8a1eeba666400b3882805b81744e4ab09d60d Mon Sep 17 00:00:00 2001 From: tofarr Date: Tue, 21 Jan 2025 12:20:35 -0700 Subject: [PATCH 12/18] Fix: Keeping runtimes alive again (For now) (#6395) --- openhands/core/config/sandbox_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands/core/config/sandbox_config.py b/openhands/core/config/sandbox_config.py index f5b984fec0b9..3ed15a01ecb2 100644 --- a/openhands/core/config/sandbox_config.py +++ b/openhands/core/config/sandbox_config.py @@ -39,7 +39,7 @@ class SandboxConfig(BaseModel): remote_runtime_api_url: str = Field(default='http://localhost:8000') local_runtime_url: str = Field(default='http://localhost') - keep_runtime_alive: bool = Field(default=False) + keep_runtime_alive: bool = Field(default=True) rm_all_containers: bool = Field(default=False) api_key: str | None = Field(default=None) base_container_image: str = Field( From b9a3f1c753c0b3ddb92ac1cf1e97ed0a2789de81 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Tue, 21 Jan 2025 21:49:30 +0100 Subject: [PATCH 13/18] Fix eval on remote runtime (#6398) --- evaluation/benchmarks/swe_bench/eval_infer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evaluation/benchmarks/swe_bench/eval_infer.py b/evaluation/benchmarks/swe_bench/eval_infer.py index 52972a920e8e..cc9dca069440 100644 --- a/evaluation/benchmarks/swe_bench/eval_infer.py +++ b/evaluation/benchmarks/swe_bench/eval_infer.py @@ -71,7 +71,7 @@ def process_git_patch(patch): return patch -def get_config(instance: pd.Series) -> AppConfig: +def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig: # 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( @@ -132,7 +132,7 @@ def process_instance( else: logger.info(f'Starting evaluation for instance {instance.instance_id}.') - config = get_config(instance) + config = get_config(metadata, instance) instance_id = instance.instance_id model_patch = instance['model_patch'] test_spec: TestSpec = instance['test_spec'] From b468150f2abf0f4c8bcf05072f808dd8a086e9c6 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Tue, 21 Jan 2025 16:54:57 -0500 Subject: [PATCH 14/18] fix(codeact): make sure agent sees the prefix/suffix as part of observation (#6400) --- openhands/agenthub/codeact_agent/codeact_agent.py | 9 ++------- openhands/events/observation/commands.py | 6 ++++-- tests/unit/test_codeact_agent.py | 13 ++++++++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/openhands/agenthub/codeact_agent/codeact_agent.py b/openhands/agenthub/codeact_agent/codeact_agent.py index 37c52855148a..ecb756781abe 100644 --- a/openhands/agenthub/codeact_agent/codeact_agent.py +++ b/openhands/agenthub/codeact_agent/codeact_agent.py @@ -277,16 +277,11 @@ def get_observation_message( # if it doesn't have tool call metadata, it was triggered by a user action if obs.tool_call_metadata is None: text = truncate_content( - f'\nObserved result of command executed by user:\n{obs.content}', + f'\nObserved result of command executed by user:\n{obs.to_agent_observation()}', max_message_chars, ) else: - text = truncate_content( - obs.content - + f'\n[Python Interpreter: {obs.metadata.py_interpreter_path}]', - max_message_chars, - ) - text += f'\n[Command finished with exit code {obs.exit_code}]' + text = truncate_content(obs.to_agent_observation(), max_message_chars) message = Message(role='user', content=[TextContent(text=text)]) elif isinstance(obs, IPythonRunCellObservation): text = obs.content diff --git a/openhands/events/observation/commands.py b/openhands/events/observation/commands.py index 31b4472f7ee7..edddb250cda5 100644 --- a/openhands/events/observation/commands.py +++ b/openhands/events/observation/commands.py @@ -149,16 +149,18 @@ def __str__(self) -> str: f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code}, ' f'metadata={json.dumps(self.metadata.model_dump(), indent=2)})**\n' '--BEGIN AGENT OBSERVATION--\n' - f'{self._to_agent_observation()}\n' + f'{self.to_agent_observation()}\n' '--END AGENT OBSERVATION--' ) - def _to_agent_observation(self) -> str: + def to_agent_observation(self) -> str: ret = f'{self.metadata.prefix}{self.content}{self.metadata.suffix}' if self.metadata.working_dir: ret += f'\n[Current working directory: {self.metadata.working_dir}]' if self.metadata.py_interpreter_path: ret += f'\n[Python interpreter: {self.metadata.py_interpreter_path}]' + if self.metadata.exit_code != -1: + ret += f'\n[Command finished with exit code {self.metadata.exit_code}]' return ret diff --git a/tests/unit/test_codeact_agent.py b/tests/unit/test_codeact_agent.py index 26fa4428826e..39badebff046 100644 --- a/tests/unit/test_codeact_agent.py +++ b/tests/unit/test_codeact_agent.py @@ -46,7 +46,7 @@ def agent() -> CodeActAgent: agent = CodeActAgent(llm=LLM(LLMConfig()), config=config) agent.llm = Mock() agent.llm.config = Mock() - agent.llm.config.max_message_chars = 100 + agent.llm.config.max_message_chars = 1000 return agent @@ -65,10 +65,15 @@ def test_cmd_output_observation_message(agent: CodeActAgent): content='Command output', metadata=CmdOutputMetadata( exit_code=0, + prefix='[THIS IS PREFIX]', + suffix='[THIS IS SUFFIX]', ), ) - results = agent.get_observation_message(obs, tool_call_id_to_message={}) + tool_call_id_to_message = {} + results = agent.get_observation_message( + obs, tool_call_id_to_message=tool_call_id_to_message + ) assert len(results) == 1 result = results[0] @@ -76,8 +81,10 @@ def test_cmd_output_observation_message(agent: CodeActAgent): assert result.role == 'user' assert len(result.content) == 1 assert isinstance(result.content[0], TextContent) - assert 'Command output' in result.content[0].text + assert 'Observed result of command executed by user:' in result.content[0].text assert '[Command finished with exit code 0]' in result.content[0].text + assert '[THIS IS PREFIX]' in result.content[0].text + assert '[THIS IS SUFFIX]' in result.content[0].text def test_ipython_run_cell_observation_message(agent: CodeActAgent): From 318c811817cab33751ef40e74d8f1b3c4b0fe831 Mon Sep 17 00:00:00 2001 From: tofarr Date: Tue, 21 Jan 2025 15:32:46 -0700 Subject: [PATCH 15/18] Added check to shutdown hook (#6402) --- openhands/runtime/impl/docker/docker_runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands/runtime/impl/docker/docker_runtime.py b/openhands/runtime/impl/docker/docker_runtime.py index bf06e00e854f..56e3f01a64e5 100644 --- a/openhands/runtime/impl/docker/docker_runtime.py +++ b/openhands/runtime/impl/docker/docker_runtime.py @@ -66,7 +66,7 @@ def __init__( headless_mode: bool = True, ): global _atexit_registered - if not _atexit_registered: + if not _atexit_registered and not config.sandbox.keep_runtime_alive: _atexit_registered = True atexit.register(remove_all_runtime_containers) From f0dbb02ee146eec0e2d63bf313302a53809023c6 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Tue, 21 Jan 2025 23:50:39 +0100 Subject: [PATCH 16/18] Adjust prompt to use view command (#5506) Co-authored-by: openhands --- openhands/agenthub/codeact_agent/prompts/system_prompt.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openhands/agenthub/codeact_agent/prompts/system_prompt.j2 b/openhands/agenthub/codeact_agent/prompts/system_prompt.j2 index 325392f2e662..84d1d6a4f78f 100644 --- a/openhands/agenthub/codeact_agent/prompts/system_prompt.j2 +++ b/openhands/agenthub/codeact_agent/prompts/system_prompt.j2 @@ -1,6 +1,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks. * If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it. +* You should start exploring the file system with your view command, unless you need to explore more deeply. * When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. -* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior. +* You MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior. From f9ba16b648160115ab7b314e92074d03d1239ee6 Mon Sep 17 00:00:00 2001 From: Boxuan Li Date: Tue, 21 Jan 2025 18:22:01 -0800 Subject: [PATCH 17/18] Edit tool prompt tweaking: only plain-text format is supported (#6067) Co-authored-by: Graham Neubig Co-authored-by: mamoodi --- openhands/agenthub/codeact_agent/function_calling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index af07393dd4c3..08709c9ee526 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -80,7 +80,7 @@ ), ) -_FILE_EDIT_DESCRIPTION = """Edit a file. +_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format. * The assistant can edit files by specifying the file path and providing a draft of the new file content. * The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections. * IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines. @@ -216,7 +216,7 @@ def __init__(self): ), ) -_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files +_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file From 04e36df4d7cc17db568a2bffa4a4647315eaacec Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Wed, 22 Jan 2025 10:26:59 -0500 Subject: [PATCH 18/18] remove dead code (#6386) --- openhands/server/auth.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/openhands/server/auth.py b/openhands/server/auth.py index 09bc0eb37981..a880cb58ae8c 100644 --- a/openhands/server/auth.py +++ b/openhands/server/auth.py @@ -1,44 +1,5 @@ -import jwt from fastapi import Request -from jwt.exceptions import InvalidTokenError - -from openhands.core.logger import openhands_logger as logger def get_user_id(request: Request) -> str | None: return getattr(request.state, 'github_user_id', None) - - -def get_sid_from_token(token: str, jwt_secret: str) -> str: - """Retrieves the session id from a JWT token. - - Parameters: - token (str): The JWT token from which the session id is to be extracted. - - Returns: - str: The session id if found and valid, otherwise an empty string. - """ - try: - # Decode the JWT using the specified secret and algorithm - payload = jwt.decode(token, jwt_secret, algorithms=['HS256']) - - # Ensure the payload contains 'sid' - if 'sid' in payload: - return payload['sid'] - else: - logger.error('SID not found in token') - return '' - except InvalidTokenError: - logger.error('Invalid token') - except Exception as e: - logger.exception('Unexpected error decoding token: %s', e) - return '' - - -def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str: - """Signs a JWT token.""" - # payload = { - # "sid": sid, - # # "exp": datetime.now(timezone.utc) + timedelta(minutes=15), - # } - return jwt.encode(payload, jwt_secret, algorithm=algorithm)