Compare commits
1 Commits
fix-markdo
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5bd2cc94 |
2
.github/workflows/ghcr-build.yml
vendored
@@ -286,6 +286,7 @@ jobs:
|
||||
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
|
||||
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
SKIP_CONTAINER_LOGS=true \
|
||||
TEST_RUNTIME=eventstream \
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
@@ -363,6 +364,7 @@ jobs:
|
||||
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
|
||||
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
SKIP_CONTAINER_LOGS=true \
|
||||
TEST_RUNTIME=eventstream \
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
|
||||
@@ -44,7 +44,6 @@ docker run -it --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
|
||||
@@ -49,7 +49,6 @@ docker run -it \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
-e LLM_MODEL=$LLM_MODEL \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
|
||||
@@ -17,7 +17,6 @@ docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# LiteLLM Proxy
|
||||
|
||||
OpenHands supports using the [LiteLLM proxy](https://docs.litellm.ai/docs/proxy/quick_start) to access various LLM providers.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use LiteLLM proxy with OpenHands, you need to:
|
||||
|
||||
1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start))
|
||||
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
* Enable `Advanced Options`
|
||||
* `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`)
|
||||
* `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`)
|
||||
* `API Key` to your LiteLLM proxy API key
|
||||
|
||||
## Supported Models
|
||||
|
||||
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy is configured to handle.
|
||||
|
||||
Refer to your LiteLLM proxy configuration for the list of available models and their names.
|
||||
@@ -63,7 +63,6 @@ We have a few guides for running OpenHands with specific model providers:
|
||||
- [Azure](llms/azure-llms)
|
||||
- [Google](llms/google-llms)
|
||||
- [Groq](llms/groq)
|
||||
- [LiteLLM Proxy](llms/litellm-proxy)
|
||||
- [OpenAI](llms/openai-llms)
|
||||
- [OpenRouter](llms/openrouter)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ docker run # ...
|
||||
-e RUNTIME=remote \
|
||||
-e SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.app.all-hands.dev" \
|
||||
-e SANDBOX_API_KEY="your-all-hands-api-key" \
|
||||
-e SANDBOX_KEEP_RUNTIME_ALIVE="true" \
|
||||
-e SANDBOX_KEEP_REMOTE_RUNTIME_ALIVE="true" \
|
||||
# ...
|
||||
```
|
||||
|
||||
|
||||
@@ -76,11 +76,6 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Groq',
|
||||
id: 'usage/llms/groq',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'LiteLLM Proxy',
|
||||
id: 'usage/llms/litellm-proxy',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'OpenAI',
|
||||
|
||||
@@ -66,7 +66,7 @@ def get_config(
|
||||
browsergym_eval_env=env_id,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
keep_remote_runtime_alive=False,
|
||||
),
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_config(
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
keep_remote_runtime_alive=False,
|
||||
),
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
import pandas as pd
|
||||
from swebench.harness.grading import get_eval_report
|
||||
@@ -95,28 +94,13 @@ def get_config(instance: pd.Series) -> AppConfig:
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
metadata: EvalMetadata | None = None,
|
||||
reset_logger: bool = True,
|
||||
log_dir: str | None = None,
|
||||
) -> EvalOutput:
|
||||
"""
|
||||
Evaluate agent performance on a SWE-bench problem instance.
|
||||
|
||||
Note that this signature differs from the expected input to `run_evaluation`. Use
|
||||
`functools.partial` to provide optional arguments before passing to the evaluation harness.
|
||||
|
||||
Args:
|
||||
log_dir (str | None, default=None): Path to directory where log files will be written. Must
|
||||
be provided if `reset_logger` is set.
|
||||
|
||||
Raises:
|
||||
AssertionError: if the `reset_logger` flag is set without a provided log directory.
|
||||
"""
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
assert (
|
||||
log_dir is not None
|
||||
), "Can't reset logger without a provided log directory."
|
||||
global output_file
|
||||
log_dir = output_file.replace('.jsonl', '.logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
|
||||
else:
|
||||
@@ -143,7 +127,6 @@ def process_instance(
|
||||
return EvalOutput(
|
||||
instance_id=instance_id,
|
||||
test_result=instance['test_result'],
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
runtime = create_runtime(config)
|
||||
@@ -193,7 +176,6 @@ def process_instance(
|
||||
return EvalOutput(
|
||||
instance_id=instance_id,
|
||||
test_result=instance['test_result'],
|
||||
metadata=metadata,
|
||||
)
|
||||
elif 'APPLY_PATCH_PASS' in apply_patch_output:
|
||||
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
|
||||
@@ -287,7 +269,6 @@ def process_instance(
|
||||
return EvalOutput(
|
||||
instance_id=instance_id,
|
||||
test_result=instance['test_result'],
|
||||
metadata=metadata,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
@@ -374,26 +355,12 @@ if __name__ == '__main__':
|
||||
output_file = args.input_file.replace('.jsonl', '.swebench_eval.jsonl')
|
||||
instances = prepare_dataset(predictions, output_file, args.eval_n_limit)
|
||||
|
||||
# If possible, load the relevant metadata to avoid issues with `run_evaluation`.
|
||||
metadata: EvalMetadata | None = None
|
||||
metadata_filepath = os.path.join(os.path.dirname(args.input_file), 'metadata.json')
|
||||
if os.path.exists(metadata_filepath):
|
||||
with open(metadata_filepath, 'r') as metadata_file:
|
||||
data = metadata_file.read()
|
||||
metadata = EvalMetadata.model_validate_json(data)
|
||||
|
||||
# The evaluation harness constrains the signature of `process_instance_func` but we need to
|
||||
# pass extra information. Build a new function object to avoid issues with multiprocessing.
|
||||
process_instance_func = partial(
|
||||
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
|
||||
)
|
||||
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata=metadata,
|
||||
metadata=None,
|
||||
output_file=output_file,
|
||||
num_workers=args.eval_num_workers,
|
||||
process_instance_func=process_instance_func,
|
||||
process_instance_func=process_instance,
|
||||
)
|
||||
|
||||
# Load evaluated predictions & print number of resolved predictions
|
||||
|
||||
@@ -36,8 +36,8 @@ from openhands.events.action import CmdRunAction, MessageAction
|
||||
from openhands.events.observation import CmdOutputObservation, ErrorObservation
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.utils.shutdown_listener import sleep_if_should_continue
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
|
||||
@@ -145,7 +145,7 @@ def get_config(
|
||||
platform='linux/amd64',
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
keep_remote_runtime_alive=False,
|
||||
remote_runtime_init_timeout=1800,
|
||||
),
|
||||
# do not mount workspace
|
||||
|
||||
@@ -346,7 +346,6 @@ def run_evaluation(
|
||||
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
|
||||
)
|
||||
else:
|
||||
logger.warning('Running evaluation without metadata.')
|
||||
logger.info(f'Evaluation started with {num_workers} workers.')
|
||||
|
||||
total_instances = len(dataset)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { clearSession } from "../src/utils/clear-session";
|
||||
import store from "../src/store";
|
||||
import { initialState as browserInitialState } from "../src/state/browserSlice";
|
||||
|
||||
describe("clearSession", () => {
|
||||
beforeEach(() => {
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
vi.stubGlobal("localStorage", localStorageMock);
|
||||
|
||||
// Set initial browser state to non-default values
|
||||
store.dispatch({
|
||||
type: "browser/setUrl",
|
||||
payload: "https://example.com",
|
||||
});
|
||||
store.dispatch({
|
||||
type: "browser/setScreenshotSrc",
|
||||
payload: "base64screenshot",
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear localStorage and reset browser state", () => {
|
||||
clearSession();
|
||||
|
||||
// Verify localStorage items were removed
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith("token");
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith("repo");
|
||||
|
||||
// Verify browser state was reset
|
||||
const state = store.getState();
|
||||
expect(state.browser.url).toBe(browserInitialState.url);
|
||||
expect(state.browser.screenshotSrc).toBe(browserInitialState.screenshotSrc);
|
||||
});
|
||||
});
|
||||
@@ -16,14 +16,14 @@ describe("Empty state", () => {
|
||||
send: vi.fn(),
|
||||
}));
|
||||
|
||||
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
|
||||
useWsClient: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
|
||||
const { useSocket: useSocketMock } = vi.hoisted(() => ({
|
||||
useSocket: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("#/context/socket", async (importActual) => ({
|
||||
...(await importActual<typeof import("#/context/ws-client-provider")>()),
|
||||
useWsClient: useWsClientMock,
|
||||
...(await importActual<typeof import("#/context/socket")>()),
|
||||
useSocket: useSocketMock,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("Empty state", () => {
|
||||
"should load the a user message to the input when selecting",
|
||||
async () => {
|
||||
// this is to test that the message is in the UI before the socket is called
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: false, // mock an inactive runtime setup
|
||||
}));
|
||||
@@ -106,7 +106,7 @@ describe("Empty state", () => {
|
||||
it.fails(
|
||||
"should send the message to the socket only if the runtime is active",
|
||||
async () => {
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: false, // mock an inactive runtime setup
|
||||
}));
|
||||
@@ -123,7 +123,7 @@ describe("Empty state", () => {
|
||||
await user.click(displayedSuggestions[0]);
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: true, // mock an active runtime setup
|
||||
}));
|
||||
|
||||
@@ -2,9 +2,8 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach } from "node:test";
|
||||
import { useTerminal } from "#/hooks/useTerminal";
|
||||
import { SocketProvider } from "#/context/socket";
|
||||
import { Command } from "#/state/commandSlice";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
@@ -19,17 +18,6 @@ function TestTerminalComponent({
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
function Wrapper({children}: WrapperProps) {
|
||||
return (
|
||||
<WsClientProvider enabled={true} token="NO_JWT" ghToken="NO_GITHUB" settings={null}>{children}</WsClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe("useTerminal", () => {
|
||||
const mockTerminal = vi.hoisted(() => ({
|
||||
loadAddon: vi.fn(),
|
||||
@@ -62,7 +50,7 @@ describe("useTerminal", () => {
|
||||
|
||||
it("should render", () => {
|
||||
render(<TestTerminalComponent commands={[]} secrets={[]} />, {
|
||||
wrapper: Wrapper,
|
||||
wrapper: SocketProvider,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +61,7 @@ describe("useTerminal", () => {
|
||||
];
|
||||
|
||||
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
|
||||
wrapper: Wrapper,
|
||||
wrapper: SocketProvider,
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
@@ -97,7 +85,7 @@ describe("useTerminal", () => {
|
||||
secrets={[secret, anotherSecret]}
|
||||
/>,
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
wrapper: SocketProvider,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
12
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
@@ -26,7 +26,7 @@
|
||||
"isbot": "^5.1.17",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"posthog-js": "^1.184.1",
|
||||
"posthog-js": "^1.176.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -19749,9 +19749,9 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.184.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.184.1.tgz",
|
||||
"integrity": "sha512-q/1Kdard5SZnL2smrzeKcD+RuUi2PnbidiN4D3ThK20bNrhy5Z2heIy9SnRMvEiARY5lcQ7zxmDCAKPBKGSOtQ==",
|
||||
"version": "1.176.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.176.0.tgz",
|
||||
"integrity": "sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -25,7 +25,7 @@
|
||||
"isbot": "^5.1.17",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"posthog-js": "^1.184.1",
|
||||
"posthog-js": "^1.176.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -120,4 +120,4 @@
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"APP_MODE": "oss",
|
||||
"GITHUB_CLIENT_ID": "",
|
||||
"POSTHOG_CLIENT_KEY": "phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA"
|
||||
}
|
||||
"GITHUB_CLIENT_ID": ""
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
GitHubAccessTokenResponse,
|
||||
ErrorResponse,
|
||||
GetConfigResponse,
|
||||
GetVSCodeUrlResponse,
|
||||
} from "./open-hands.types";
|
||||
|
||||
class OpenHands {
|
||||
@@ -175,14 +174,6 @@ class OpenHands {
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the VSCode URL
|
||||
* @returns VSCode URL
|
||||
*/
|
||||
static async getVSCodeUrl(): Promise<GetVSCodeUrlResponse> {
|
||||
return request(`/api/vscode-url`, {}, false, false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -43,11 +43,5 @@ export interface Feedback {
|
||||
|
||||
export interface GetConfigResponse {
|
||||
APP_MODE: "saas" | "oss";
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
}
|
||||
|
||||
export interface GetVSCodeUrlResponse {
|
||||
vscode_url: string | null;
|
||||
error?: string;
|
||||
GITHUB_CLIENT_ID: string | null;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 662 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 552 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 924 B After Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 856 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
@@ -1,57 +0,0 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<g filter="url(#filter0_d)">
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.5723C72.4869 100.189 74.2828 100.15 75.8725 99.3807L96.4604 89.4231C98.624 88.3771 100 86.1762 100 83.7616V16.2392C100 13.8247 98.624 11.6238 96.4604 10.5774L75.8725 0.619067C73.7862 -0.389991 71.3446 -0.142885 69.5135 1.19527C69.252 1.38636 69.0028 1.59985 68.769 1.83502L29.3551 37.9795L12.1872 24.88C10.5891 23.6607 8.35365 23.7606 6.86938 25.1178L1.36302 30.1525C-0.452603 31.8127 -0.454583 34.6837 1.35854 36.3466L16.2471 50.0001L1.35854 63.6536C-0.454583 65.3164 -0.452603 68.1876 1.36302 69.8477L6.86938 74.8824C8.35365 76.2395 10.5891 76.34 12.1872 75.1201L29.3551 62.0207L68.769 98.1651C69.3925 98.7923 70.1246 99.2645 70.9119 99.5723ZM75.0152 27.1813L45.1092 50.0001L75.0152 72.8189V27.1813Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M96.4614 10.593L75.8567 0.62085C73.4717 -0.533437 70.6215 -0.0465506 68.7498 1.83492L1.29834 63.6535C-0.515935 65.3164 -0.513852 68.1875 1.30281 69.8476L6.8125 74.8823C8.29771 76.2395 10.5345 76.339 12.1335 75.1201L93.3604 13.18C96.0854 11.102 100 13.0557 100 16.4939V16.2535C100 13.84 98.6239 11.64 96.4614 10.593Z" fill="#D9D9D9"/>
|
||||
<g filter="url(#filter1_d)">
|
||||
<path d="M96.4614 89.4074L75.8567 99.3797C73.4717 100.534 70.6215 100.047 68.7498 98.1651L1.29834 36.3464C-0.515935 34.6837 -0.513852 31.8125 1.30281 30.1524L6.8125 25.1177C8.29771 23.7605 10.5345 23.6606 12.1335 24.88L93.3604 86.8201C96.0854 88.8985 100 86.9447 100 83.5061V83.747C100 86.1604 98.6239 88.3603 96.4614 89.4074Z" fill="#E6E6E6"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d)">
|
||||
<path d="M75.8578 99.3807C73.4721 100.535 70.6219 100.047 68.75 98.1651C71.0564 100.483 75 98.8415 75 95.5631V4.43709C75 1.15852 71.0565 -0.483493 68.75 1.83492C70.6219 -0.0467614 73.4721 -0.534276 75.8578 0.618963L96.4583 10.5773C98.6229 11.6237 100 13.8246 100 16.2391V83.7616C100 86.1762 98.6229 88.3761 96.4583 89.4231L75.8578 99.3807Z" fill="white"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:overlay" opacity="0.25">
|
||||
<path style="mix-blend-mode:overlay" opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M70.8508 99.5723C72.4258 100.189 74.2218 100.15 75.8115 99.3807L96.4 89.4231C98.5635 88.3771 99.9386 86.1762 99.9386 83.7616V16.2391C99.9386 13.8247 98.5635 11.6239 96.4 10.5774L75.8115 0.618974C73.7252 -0.390085 71.2835 -0.142871 69.4525 1.19518C69.1909 1.38637 68.9418 1.59976 68.7079 1.83493L29.2941 37.9795L12.1261 24.88C10.528 23.6606 8.2926 23.7605 6.80833 25.1177L1.30198 30.1524C-0.51354 31.8126 -0.515625 34.6837 1.2975 36.3465L16.186 50L1.2975 63.6536C-0.515625 65.3164 -0.51354 68.1875 1.30198 69.8476L6.80833 74.8824C8.2926 76.2395 10.528 76.339 12.1261 75.1201L29.2941 62.0207L68.7079 98.1651C69.3315 98.7923 70.0635 99.2645 70.8508 99.5723ZM74.9542 27.1812L45.0481 50L74.9542 72.8188V27.1812Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="-6.25" y="-4.16667" width="112.5" height="112.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="2.08333"/>
|
||||
<feGaussianBlur stdDeviation="3.125"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="-8.39436" y="15.6951" width="116.728" height="92.6376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d" x="60.4167" y="-8.33346" width="47.9167" height="116.667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="49.939" y1="-5.19792e-05" x2="49.939" y2="100.001" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="100" height="100" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -6,7 +6,7 @@ import PlayIcon from "#/assets/play";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
|
||||
import { RootState } from "#/store";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useSocket } from "#/context/socket";
|
||||
|
||||
const IgnoreTaskStateMap: Record<string, AgentState[]> = {
|
||||
[AgentState.PAUSED]: [
|
||||
@@ -72,7 +72,7 @@ function ActionButton({
|
||||
}
|
||||
|
||||
function AgentControlBar() {
|
||||
const { send } = useWsClient();
|
||||
const { send } = useSocket();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const handleAction = (action: AgentState) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Clip from "#/icons/clip.svg?react";
|
||||
import Clip from "#/assets/clip.svg?react";
|
||||
|
||||
export function AttachImageLabel() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
|
||||
import ArrowSendIcon from "#/assets/arrow-send.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ChatInputProps {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { FeedbackActions } from "./feedback-actions";
|
||||
@@ -20,15 +21,14 @@ import { ContinueButton } from "./continue-button";
|
||||
import { ScrollToBottomButton } from "./scroll-to-bottom-button";
|
||||
import { Suggestions } from "./suggestions";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import BuildIt from "#/assets/build-it.svg?react";
|
||||
|
||||
const isErrorMessage = (
|
||||
message: Message | ErrorMessage,
|
||||
): message is ErrorMessage => "error" in message;
|
||||
|
||||
export function ChatInterface() {
|
||||
const { send } = useWsClient();
|
||||
const { send } = useSocket();
|
||||
const dispatch = useDispatch();
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
||||
|
||||
@@ -5,7 +5,7 @@ import RejectIcon from "#/assets/reject";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useSocket } from "#/context/socket";
|
||||
|
||||
interface ActionTooltipProps {
|
||||
type: "confirm" | "reject";
|
||||
@@ -37,7 +37,7 @@ function ActionTooltip({ type, onClick }: ActionTooltipProps) {
|
||||
|
||||
function ConfirmationButtons() {
|
||||
const { t } = useTranslation();
|
||||
const { send } = useWsClient();
|
||||
const { send } = useSocket();
|
||||
|
||||
const handleStateChange = (state: AgentState) => {
|
||||
const event = generateAgentStateChangeEvent(state);
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { addErrorMessage, addUserMessage } from "#/state/chatSlice";
|
||||
import {
|
||||
getCloneRepoCommand,
|
||||
getGitHubTokenCommand,
|
||||
} from "#/services/terminalService";
|
||||
import {
|
||||
clearFiles,
|
||||
clearSelectedRepository,
|
||||
setImportedProjectZip,
|
||||
} from "#/state/initial-query-slice";
|
||||
import { clientLoader as appClientLoader } from "#/routes/_oh.app";
|
||||
import store, { RootState } from "#/store";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import { clientLoader as rootClientLoader } from "#/routes/_oh";
|
||||
import { isGitHubErrorReponse } from "#/api/github";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { setCurrentAgentState } from "#/state/agentSlice";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { getSettings } from "#/services/settings";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
message: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const isServerError = (data: object): data is ServerError => "error" in data;
|
||||
|
||||
const isErrorObservation = (data: object): data is ErrorObservation =>
|
||||
"observation" in data && data.observation === "error";
|
||||
|
||||
export function EventHandler({ children }: React.PropsWithChildren) {
|
||||
const { events, status, send } = useWsClient();
|
||||
const statusRef = React.useRef<WsClientProviderStatus | null>(null);
|
||||
const runtimeActive = status === WsClientProviderStatus.ACTIVE;
|
||||
const fetcher = useFetcher();
|
||||
const dispatch = useDispatch();
|
||||
const { files, importedProjectZip } = useSelector(
|
||||
(state: RootState) => state.initalQuery,
|
||||
);
|
||||
const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
|
||||
const initialQueryRef = React.useRef<string | null>(
|
||||
store.getState().initalQuery.initialQuery,
|
||||
);
|
||||
|
||||
const sendInitialQuery = (query: string, base64Files: string[]) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
send(createChatMessage(query, base64Files, timestamp));
|
||||
};
|
||||
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
|
||||
const userId = React.useMemo(() => {
|
||||
if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
|
||||
return null;
|
||||
}, [data?.user]);
|
||||
const userSettings = getSettings();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
const event = events[events.length - 1];
|
||||
if (event.token) {
|
||||
fetcher.submit({ token: event.token as string }, { method: "post" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
fetcher.submit({}, { method: "POST", action: "/end-session" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.error === "string") {
|
||||
toast.error(event.error);
|
||||
} else {
|
||||
toast.error(event.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
const message: string = `${event.message}`;
|
||||
if (message.startsWith("Agent reached maximum")) {
|
||||
// We set the agent state to paused here - if the user clicks resume, it auto updates the max iterations
|
||||
send(generateAgentStateChangeEvent(AgentState.PAUSED));
|
||||
}
|
||||
}
|
||||
|
||||
if (isErrorObservation(event)) {
|
||||
dispatch(
|
||||
addErrorMessage({
|
||||
id: event.extras?.error_id,
|
||||
message: event.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [events.length]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (statusRef.current === status) {
|
||||
return; // This is a check because of strict mode - if the status did not change, don't do anything
|
||||
}
|
||||
statusRef.current = status;
|
||||
const initialQuery = initialQueryRef.current;
|
||||
|
||||
if (status === WsClientProviderStatus.ACTIVE) {
|
||||
let additionalInfo = "";
|
||||
if (ghToken && repo) {
|
||||
send(getCloneRepoCommand(ghToken, repo));
|
||||
additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
|
||||
dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
|
||||
}
|
||||
// if there's an uploaded project zip, add it to the chat
|
||||
else if (importedProjectZip) {
|
||||
additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
|
||||
}
|
||||
|
||||
if (initialQuery) {
|
||||
if (additionalInfo) {
|
||||
sendInitialQuery(`${initialQuery}\n\n[${additionalInfo}]`, files);
|
||||
} else {
|
||||
sendInitialQuery(initialQuery, files);
|
||||
}
|
||||
dispatch(clearFiles()); // reset selected files
|
||||
initialQueryRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (status === WsClientProviderStatus.OPENING && initialQuery) {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: initialQuery,
|
||||
imageUrls: files,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (status === WsClientProviderStatus.STOPPED) {
|
||||
store.dispatch(setCurrentAgentState(AgentState.STOPPED));
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (runtimeActive && userId && ghToken) {
|
||||
// Export if the user valid, this could happen mid-session so it is handled here
|
||||
send(getGitHubTokenCommand(ghToken));
|
||||
}
|
||||
}, [userId, ghToken, runtimeActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (runtimeActive && importedProjectZip) {
|
||||
// upload files action
|
||||
try {
|
||||
const blob = base64ToBlob(importedProjectZip);
|
||||
const file = new File([blob], "imported-project.zip", {
|
||||
type: blob.type,
|
||||
});
|
||||
await OpenHands.uploadFiles([file]);
|
||||
dispatch(setImportedProjectZip(null));
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload project files.");
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [runtimeActive, importedProjectZip]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (userSettings.LLM_API_KEY) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [userSettings.LLM_API_KEY]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { setRefreshID } from "#/state/codeSlice";
|
||||
import { addAssistantMessage } from "#/state/chatSlice";
|
||||
import IconButton from "../IconButton";
|
||||
import ExplorerTree from "./ExplorerTree";
|
||||
import toast from "#/utils/toast";
|
||||
@@ -21,7 +20,6 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useFiles } from "#/context/files";
|
||||
import { isOpenHandsErrorResponse } from "#/api/open-hands.utils";
|
||||
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
|
||||
|
||||
interface ExplorerActionsProps {
|
||||
onRefresh: () => void;
|
||||
@@ -170,35 +168,6 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVSCodeClick = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await OpenHands.getVSCodeUrl();
|
||||
if (response.vscode_url) {
|
||||
dispatch(
|
||||
addAssistantMessage(
|
||||
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
|
||||
),
|
||||
);
|
||||
window.open(response.vscode_url, "_blank");
|
||||
} else {
|
||||
toast.error(
|
||||
`open-vscode-error-${new Date().getTime()}`,
|
||||
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
|
||||
error: response.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (exp_error) {
|
||||
toast.error(
|
||||
`open-vscode-error-${new Date().getTime()}`,
|
||||
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
|
||||
error: String(exp_error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshWorkspace();
|
||||
}, [curAgentState]);
|
||||
@@ -241,7 +210,7 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
|
||||
!isOpen ? "w-12" : "w-60",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
|
||||
<div className="flex flex-col relative h-full px-3 py-2">
|
||||
<div className="sticky top-0 bg-neutral-800">
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -263,7 +232,7 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
|
||||
</div>
|
||||
</div>
|
||||
{!error && (
|
||||
<div className="overflow-auto flex-grow min-h-0">
|
||||
<div className="overflow-auto flex-grow">
|
||||
<div style={{ display: !isOpen ? "none" : "block" }}>
|
||||
<ExplorerTree files={paths} />
|
||||
</div>
|
||||
@@ -274,27 +243,6 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
|
||||
<p className="text-neutral-300 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{isOpen && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleVSCodeClick}
|
||||
disabled={
|
||||
curAgentState === AgentState.INIT ||
|
||||
curAgentState === AgentState.LOADING
|
||||
}
|
||||
className={twMerge(
|
||||
"mt-auto mb-2 w-full h-10 text-white rounded flex items-center justify-center gap-2 transition-colors",
|
||||
curAgentState === AgentState.INIT ||
|
||||
curAgentState === AgentState.LOADING
|
||||
? "bg-neutral-600 cursor-not-allowed"
|
||||
: "bg-[#4465DB] hover:bg-[#3451C7]",
|
||||
)}
|
||||
aria-label="Open in VS Code"
|
||||
>
|
||||
<VSCodeIcon width={20} height={20} />
|
||||
Open in VS Code
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
data-testid="file-input"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import CloseIcon from "#/icons/close.svg?react";
|
||||
import CloseIcon from "#/assets/close.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ImagePreviewProps {
|
||||
|
||||
@@ -59,6 +59,11 @@ export function InteractiveChatBox({
|
||||
"bg-neutral-700 border border-neutral-600 rounded-lg px-2 py-[10px]",
|
||||
"transition-colors duration-200",
|
||||
"hover:border-neutral-500 focus-within:border-neutral-500",
|
||||
"group relative",
|
||||
"before:pointer-events-none before:absolute before:inset-0 before:rounded-lg before:transition-colors",
|
||||
"before:border-2 before:border-dashed before:border-transparent",
|
||||
"[&:has(*:focus-within)]:before:border-neutral-500/50",
|
||||
"[&:has(*[data-dragging-over='true'])]:before:border-neutral-500/50",
|
||||
)}
|
||||
>
|
||||
<UploadImageInput onUpload={handleUpload} />
|
||||
|
||||
@@ -17,11 +17,6 @@ export function ol({
|
||||
React.HTMLAttributes<HTMLElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<ol
|
||||
className="list-decimal ml-5 pl-2 whitespace-normal"
|
||||
style={{ counterReset: "list-item" }}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
<ol className="list-decimal ml-5 pl-2 whitespace-normal">{children}</ol>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LoadingSpinnerOuter from "#/icons/loading-outer.svg?react";
|
||||
import LoadingSpinnerOuter from "#/assets/loading-outer.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import ModalBody from "./ModalBody";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@@ -2,17 +2,17 @@ import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import posthog from "posthog-js";
|
||||
import EllipsisH from "#/icons/ellipsis-h.svg?react";
|
||||
import EllipsisH from "#/assets/ellipsis-h.svg?react";
|
||||
import { ModalBackdrop } from "../modals/modal-backdrop";
|
||||
import { ConnectToGitHubModal } from "../modals/connect-to-github-modal";
|
||||
import { addUserMessage } from "#/state/chatSlice";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
|
||||
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
|
||||
import { ProjectMenuDetails } from "./project-menu-details";
|
||||
import { downloadWorkspace } from "#/utils/download-workspace";
|
||||
import { LoadingSpinner } from "../modals/LoadingProject";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
interface ProjectMenuCardProps {
|
||||
isConnectedToGitHub: boolean;
|
||||
@@ -27,7 +27,7 @@ export function ProjectMenuCard({
|
||||
isConnectedToGitHub,
|
||||
githubData,
|
||||
}: ProjectMenuCardProps) {
|
||||
const { send } = useWsClient();
|
||||
const { send } = useSocket();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "#/utils/utils";
|
||||
import CloudConnection from "#/icons/cloud-connection.svg?react";
|
||||
import CloudConnection from "#/assets/cloud-connection.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ProjectMenuDetailsPlaceholderProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExternalLinkIcon from "#/icons/external-link.svg?react";
|
||||
import ExternalLinkIcon from "#/assets/external-link.svg?react";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
|
||||
import ArrowSendIcon from "#/assets/arrow-send.svg?react";
|
||||
|
||||
interface ScrollToBottomButtonProps {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Lightbulb from "#/icons/lightbulb.svg?react";
|
||||
import Refresh from "#/icons/refresh.svg?react";
|
||||
import Lightbulb from "#/assets/lightbulb.svg?react";
|
||||
import Refresh from "#/assets/refresh.svg?react";
|
||||
|
||||
interface SuggestionBubbleProps {
|
||||
suggestion: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Clip from "#/icons/clip.svg?react";
|
||||
import Clip from "#/assets/clip.svg?react";
|
||||
|
||||
interface UploadImageInputProps {
|
||||
onUpload: (files: File[]) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoadingSpinner } from "./modals/LoadingProject";
|
||||
import DefaultUserAvatar from "#/icons/default-user.svg?react";
|
||||
import DefaultUserAvatar from "#/assets/default-user.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface UserAvatarProps {
|
||||
|
||||
146
frontend/src/context/socket.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from "react";
|
||||
import { Data } from "ws";
|
||||
import posthog from "posthog-js";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
|
||||
interface WebSocketClientOptions {
|
||||
token: string | null;
|
||||
onOpen?: (event: Event) => void;
|
||||
onMessage?: (event: MessageEvent<Data>) => void;
|
||||
onError?: (event: Event) => void;
|
||||
onClose?: (event: Event) => void;
|
||||
}
|
||||
|
||||
interface WebSocketContextType {
|
||||
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
|
||||
start: (options?: WebSocketClientOptions) => void;
|
||||
stop: () => void;
|
||||
setRuntimeIsInitialized: () => void;
|
||||
runtimeActive: boolean;
|
||||
isConnected: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
const SocketContext = React.createContext<WebSocketContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface SocketProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SocketProvider({ children }: SocketProviderProps) {
|
||||
const wsRef = React.useRef<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = React.useState(false);
|
||||
const [runtimeActive, setRuntimeActive] = React.useState(false);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const setRuntimeIsInitialized = () => {
|
||||
setRuntimeActive(true);
|
||||
};
|
||||
|
||||
const start = React.useCallback((options?: WebSocketClientOptions): void => {
|
||||
if (wsRef.current) {
|
||||
EventLogger.warning(
|
||||
"WebSocket connection is already established, but a new one is starting anyways.",
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const sessionToken = options?.token || "NO_JWT"; // not allowed to be empty or duplicated
|
||||
const ghToken = localStorage.getItem("ghToken") || "NO_GITHUB";
|
||||
|
||||
const ws = new WebSocket(`${protocol}//${baseUrl}/ws`, [
|
||||
"openhands",
|
||||
sessionToken,
|
||||
ghToken,
|
||||
]);
|
||||
|
||||
ws.addEventListener("open", (event) => {
|
||||
posthog.capture("socket_opened");
|
||||
setIsConnected(true);
|
||||
options?.onOpen?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
EventLogger.message(event);
|
||||
|
||||
setEvents((prevEvents) => [...prevEvents, JSON.parse(event.data)]);
|
||||
options?.onMessage?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", (event) => {
|
||||
posthog.capture("socket_error");
|
||||
EventLogger.event(event, "SOCKET ERROR");
|
||||
options?.onError?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
posthog.capture("socket_closed");
|
||||
EventLogger.event(event, "SOCKET CLOSE");
|
||||
|
||||
setIsConnected(false);
|
||||
setRuntimeActive(false);
|
||||
wsRef.current = null;
|
||||
options?.onClose?.(event);
|
||||
});
|
||||
|
||||
wsRef.current = ws;
|
||||
}, []);
|
||||
|
||||
const stop = React.useCallback((): void => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const send = React.useCallback(
|
||||
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
|
||||
if (!wsRef.current) {
|
||||
EventLogger.error("WebSocket is not connected.");
|
||||
return;
|
||||
}
|
||||
setEvents((prevEvents) => [...prevEvents, JSON.parse(data.toString())]);
|
||||
wsRef.current.send(data);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
send,
|
||||
start,
|
||||
stop,
|
||||
setRuntimeIsInitialized,
|
||||
runtimeActive,
|
||||
isConnected,
|
||||
events,
|
||||
}),
|
||||
[
|
||||
send,
|
||||
start,
|
||||
stop,
|
||||
setRuntimeIsInitialized,
|
||||
runtimeActive,
|
||||
isConnected,
|
||||
events,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useSocket() {
|
||||
const context = React.useContext(SocketContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { SocketProvider, useSocket };
|
||||
@@ -1,196 +0,0 @@
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import { Settings } from "#/services/settings";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
|
||||
const RECONNECT_RETRIES = 5;
|
||||
|
||||
export enum WsClientProviderStatus {
|
||||
STOPPED,
|
||||
OPENING,
|
||||
ACTIVE,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
interface UseWsClient {
|
||||
status: WsClientProviderStatus;
|
||||
events: Record<string, unknown>[];
|
||||
send: (event: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
status: WsClientProviderStatus.STOPPED,
|
||||
events: [],
|
||||
send: () => {
|
||||
throw new Error("not connected");
|
||||
},
|
||||
});
|
||||
|
||||
interface WsClientProviderProps {
|
||||
enabled: boolean;
|
||||
token: string | null;
|
||||
ghToken: string | null;
|
||||
settings: Settings | null;
|
||||
}
|
||||
|
||||
export function WsClientProvider({
|
||||
enabled,
|
||||
token,
|
||||
ghToken,
|
||||
settings,
|
||||
children,
|
||||
}: React.PropsWithChildren<WsClientProviderProps>) {
|
||||
const wsRef = React.useRef<WebSocket | null>(null);
|
||||
const tokenRef = React.useRef<string | null>(token);
|
||||
const ghTokenRef = React.useRef<string | null>(ghToken);
|
||||
const closeRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [status, setStatus] = React.useState(WsClientProviderStatus.STOPPED);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [retryCount, setRetryCount] = React.useState(RECONNECT_RETRIES);
|
||||
|
||||
function send(event: Record<string, unknown>) {
|
||||
if (!wsRef.current) {
|
||||
EventLogger.error("WebSocket is not connected.");
|
||||
return;
|
||||
}
|
||||
wsRef.current.send(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
setRetryCount(RECONNECT_RETRIES);
|
||||
setStatus(WsClientProviderStatus.OPENING);
|
||||
const initEvent = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
send(initEvent);
|
||||
}
|
||||
|
||||
function handleMessage(messageEvent: MessageEvent) {
|
||||
const event = JSON.parse(messageEvent.data);
|
||||
setEvents((prevEvents) => [...prevEvents, event]);
|
||||
if (event.extras?.agent_state === AgentState.INIT) {
|
||||
setStatus(WsClientProviderStatus.ACTIVE);
|
||||
}
|
||||
if (
|
||||
status !== WsClientProviderStatus.ACTIVE &&
|
||||
event?.observation === "error"
|
||||
) {
|
||||
setStatus(WsClientProviderStatus.ERROR);
|
||||
}
|
||||
|
||||
handleAssistantMessage(event);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
if (retryCount) {
|
||||
setTimeout(() => {
|
||||
setRetryCount(retryCount - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setStatus(WsClientProviderStatus.STOPPED);
|
||||
setEvents([]);
|
||||
}
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
function handleError(event: Event) {
|
||||
posthog.capture("socket_error");
|
||||
EventLogger.event(event, "SOCKET ERROR");
|
||||
setStatus(WsClientProviderStatus.ERROR);
|
||||
}
|
||||
|
||||
// Connect websocket
|
||||
React.useEffect(() => {
|
||||
let ws = wsRef.current;
|
||||
|
||||
// If disabled close any existing websockets...
|
||||
if (!enabled || !retryCount) {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
wsRef.current = null;
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// If there is no websocket or the tokens have changed or the current websocket is closed,
|
||||
// create a new one
|
||||
if (
|
||||
!ws ||
|
||||
(tokenRef.current && token !== tokenRef.current) ||
|
||||
ghToken !== ghTokenRef.current ||
|
||||
ws.readyState === WebSocket.CLOSED ||
|
||||
ws.readyState === WebSocket.CLOSING
|
||||
) {
|
||||
ws?.close();
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
let wsUrl = `${protocol}//${baseUrl}/ws`;
|
||||
if (events.length) {
|
||||
wsUrl += `?latest_event_id=${events[events.length - 1].id}`;
|
||||
}
|
||||
ws = new WebSocket(wsUrl, [
|
||||
"openhands",
|
||||
token || "NO_JWT",
|
||||
ghToken || "NO_GITHUB",
|
||||
]);
|
||||
}
|
||||
ws.addEventListener("open", handleOpen);
|
||||
ws.addEventListener("message", handleMessage);
|
||||
ws.addEventListener("error", handleError);
|
||||
ws.addEventListener("close", handleClose);
|
||||
wsRef.current = ws;
|
||||
tokenRef.current = token;
|
||||
ghTokenRef.current = ghToken;
|
||||
|
||||
return () => {
|
||||
ws.removeEventListener("open", handleOpen);
|
||||
ws.removeEventListener("message", handleMessage);
|
||||
ws.removeEventListener("error", handleError);
|
||||
ws.removeEventListener("close", handleClose);
|
||||
};
|
||||
}, [enabled, token, ghToken, retryCount]);
|
||||
|
||||
// Strict mode mounts and unmounts each component twice, so we have to wait in the destructor
|
||||
// before actually closing the socket and cancel the operation if the component gets remounted.
|
||||
React.useEffect(() => {
|
||||
const timeout = closeRef.current;
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
return () => {
|
||||
closeRef.current = setTimeout(() => {
|
||||
const ws = wsRef.current;
|
||||
if (ws) {
|
||||
ws.removeEventListener("close", handleClose);
|
||||
ws.close();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo<UseWsClient>(
|
||||
() => ({
|
||||
status,
|
||||
events,
|
||||
send,
|
||||
}),
|
||||
[status, events],
|
||||
);
|
||||
|
||||
return (
|
||||
<WsClientContext.Provider value={value}>
|
||||
{children}
|
||||
</WsClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWsClient() {
|
||||
const context = React.useContext(WsClientContext);
|
||||
return context;
|
||||
}
|
||||
@@ -10,28 +10,18 @@ import React, { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { SocketProvider } from "./context/socket";
|
||||
import "./i18n";
|
||||
import store from "./store";
|
||||
import OpenHands from "./api/open-hands";
|
||||
|
||||
function PosthogInit() {
|
||||
const [key, setKey] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
OpenHands.getConfig().then((config) => {
|
||||
setKey(config.POSTHOG_CLIENT_KEY);
|
||||
posthog.init("phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA", {
|
||||
api_host: "https://us.i.posthog.com",
|
||||
person_profiles: "identified_only",
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (key) {
|
||||
posthog.init(key, {
|
||||
api_host: "https://us.i.posthog.com",
|
||||
person_profiles: "identified_only",
|
||||
});
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -53,10 +43,12 @@ prepareApp().then(() =>
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<RemixBrowser />
|
||||
<PosthogInit />
|
||||
</Provider>
|
||||
<SocketProvider>
|
||||
<Provider store={store}>
|
||||
<RemixBrowser />
|
||||
<PosthogInit />
|
||||
</Provider>
|
||||
</SocketProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import { Command } from "#/state/commandSlice";
|
||||
import { getTerminalCommand } from "#/services/terminalService";
|
||||
import { parseTerminalOutput } from "#/utils/parseTerminalOutput";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useSocket } from "#/context/socket";
|
||||
|
||||
/*
|
||||
NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component.
|
||||
@@ -15,7 +15,7 @@ export const useTerminal = (
|
||||
commands: Command[] = [],
|
||||
secrets: string[] = [],
|
||||
) => {
|
||||
const { send } = useWsClient();
|
||||
const { send } = useSocket();
|
||||
const terminal = React.useRef<Terminal | null>(null);
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -678,16 +678,6 @@
|
||||
"tr": "Sunucudan beklenmeyen yanıt yapısı",
|
||||
"no": "Uventet responsstruktur fra serveren"
|
||||
},
|
||||
"EXPLORER$VSCODE_SWITCHING_MESSAGE": {
|
||||
"en": "Switching to VS Code in 3 seconds...\nImportant: Please inform the agent of any changes you make in VS Code. To avoid conflicts, wait for the assistant to complete its work before making your own changes.",
|
||||
"zh-CN": "3 秒后切换到 VS Code\n重要提示:请告知 OpenHands 您在 VS Code 中进行的任何更改。为了避免冲突,请在 OpenHands 完成工作后再进行自己的更改。",
|
||||
"zh-TW": "3 秒後切換到 VS Code\n重要提示:請告知 OpenHands 您在 VS Code 中進行的任何更改。為避免衝突,請在 OpenHands 完成工作後再進行自己的更改。"
|
||||
},
|
||||
"EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE": {
|
||||
"en": "Error switching to VS Code: {{error}}",
|
||||
"zh-CN": "切换到 VS Code 时发生错误: {{error}}",
|
||||
"zh-TW": "切換到 VS Code 時發生錯誤: {{error}}"
|
||||
},
|
||||
"LOAD_SESSION$MODAL_TITLE": {
|
||||
"en": "Return to existing session?",
|
||||
"de": "Zurück zu vorhandener Sitzung?",
|
||||
|
||||
@@ -71,6 +71,8 @@ const openHandsHandlers = [
|
||||
export const handlers = [
|
||||
...openHandsHandlers,
|
||||
http.get("https://api.github.com/user/repos", async ({ request }) => {
|
||||
if (import.meta.env.MODE !== "test") await delay(3500);
|
||||
|
||||
const token = request.headers
|
||||
.get("Authorization")
|
||||
?.replace("Bearer", "")
|
||||
|
||||
@@ -29,7 +29,7 @@ const generateAgentResponse = (message: string): AssistantMessageAction => ({
|
||||
action: "message",
|
||||
args: {
|
||||
content: message,
|
||||
image_urls: [],
|
||||
images_urls: [],
|
||||
wait_for_response: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import BuildIt from "#/assets/build-it.svg?react";
|
||||
|
||||
export function HeroHeading() {
|
||||
return (
|
||||
|
||||
@@ -70,6 +70,11 @@ export function TaskForm() {
|
||||
"border border-neutral-600 px-4 py-[17px] rounded-lg text-[17px] leading-5 w-full transition-colors duration-200",
|
||||
inputIsFocused ? "bg-neutral-600" : "bg-neutral-700",
|
||||
"hover:border-neutral-500 focus-within:border-neutral-500",
|
||||
"group relative",
|
||||
"before:pointer-events-none before:absolute before:inset-0 before:rounded-lg before:transition-colors",
|
||||
"before:border-2 before:border-dashed before:border-transparent",
|
||||
"[&:has(*:focus-within)]:before:border-neutral-500/50",
|
||||
"[&:has(*[data-dragging-over='true'])]:before:border-neutral-500/50",
|
||||
)}
|
||||
>
|
||||
<ChatInput
|
||||
|
||||
@@ -29,10 +29,6 @@ function CodeEditorCompoonent({
|
||||
if (selectedPath && value) modifyFileContent(selectedPath, value);
|
||||
};
|
||||
|
||||
const isBase64Image = (content: string) => content.startsWith("data:image/");
|
||||
const isPDF = (content: string) => content.startsWith("data:application/pdf");
|
||||
const isVideo = (content: string) => content.startsWith("data:video/");
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSave = async (event: KeyboardEvent) => {
|
||||
if (selectedPath && event.metaKey && event.key === "s") {
|
||||
@@ -66,40 +62,16 @@ function CodeEditorCompoonent({
|
||||
);
|
||||
}
|
||||
|
||||
const fileContent = modifiedFiles[selectedPath] || files[selectedPath];
|
||||
|
||||
if (isBase64Image(fileContent)) {
|
||||
return (
|
||||
<section className="flex flex-col relative items-center overflow-auto h-[90%]">
|
||||
<img src={fileContent} alt={selectedPath} className="object-contain" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPDF(fileContent)) {
|
||||
return (
|
||||
<iframe
|
||||
src={fileContent}
|
||||
title={selectedPath}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isVideo(fileContent)) {
|
||||
return (
|
||||
<video controls src={fileContent} width="100%" height="100%">
|
||||
<track kind="captions" label="English captions" />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Editor
|
||||
data-testid="code-editor"
|
||||
path={selectedPath ?? undefined}
|
||||
defaultValue=""
|
||||
value={selectedPath ? fileContent : undefined}
|
||||
value={
|
||||
selectedPath
|
||||
? modifiedFiles[selectedPath] || files[selectedPath]
|
||||
: undefined
|
||||
}
|
||||
onMount={onMount}
|
||||
onChange={handleEditorChange}
|
||||
options={{ readOnly: isReadOnly }}
|
||||
|
||||
@@ -2,29 +2,71 @@ import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import {
|
||||
Outlet,
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
json,
|
||||
ClientActionFunctionArgs,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import WebSocket from "ws";
|
||||
import toast from "react-hot-toast";
|
||||
import { getSettings } from "#/services/settings";
|
||||
import Security from "../components/modals/security/Security";
|
||||
import { Controls } from "#/components/controls";
|
||||
import store from "#/store";
|
||||
import store, { RootState } from "#/store";
|
||||
import { Container } from "#/components/container";
|
||||
import { clearMessages } from "#/state/chatSlice";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import {
|
||||
addErrorMessage,
|
||||
addUserMessage,
|
||||
clearMessages,
|
||||
} from "#/state/chatSlice";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import {
|
||||
getGitHubTokenCommand,
|
||||
getCloneRepoCommand,
|
||||
} from "#/services/terminalService";
|
||||
import { clearTerminal } from "#/state/commandSlice";
|
||||
import { useEffectOnce } from "#/utils/use-effect-once";
|
||||
import CodeIcon from "#/icons/code.svg?react";
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import ListIcon from "#/icons/list-type-number.svg?react";
|
||||
import { clearInitialQuery } from "#/state/initial-query-slice";
|
||||
import CodeIcon from "#/assets/code.svg?react";
|
||||
import GlobeIcon from "#/assets/globe.svg?react";
|
||||
import ListIcon from "#/assets/list-type-number.svg?react";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import {
|
||||
clearFiles,
|
||||
clearInitialQuery,
|
||||
clearSelectedRepository,
|
||||
setImportedProjectZip,
|
||||
} from "#/state/initial-query-slice";
|
||||
import { isGitHubErrorReponse, retrieveLatestGitHubCommit } from "#/api/github";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { clientLoader as rootClientLoader } from "#/routes/_oh";
|
||||
import { clearJupyter } from "#/state/jupyterSlice";
|
||||
import { FilesProvider } from "#/context/files";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { ChatInterface } from "#/components/chat-interface";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { EventHandler } from "#/components/event-handler";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
message: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const isServerError = (data: object): data is ServerError => "error" in data;
|
||||
|
||||
const isErrorObservation = (data: object): data is ErrorObservation =>
|
||||
"observation" in data && data.observation === "error";
|
||||
|
||||
const isAgentStateChange = (
|
||||
data: object,
|
||||
): data is { extras: { agent_state: AgentState } } =>
|
||||
"extras" in data &&
|
||||
data.extras instanceof Object &&
|
||||
"agent_state" in data.extras;
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const ghToken = localStorage.getItem("ghToken");
|
||||
@@ -74,26 +116,174 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
|
||||
|
||||
function App() {
|
||||
const dispatch = useDispatch();
|
||||
const { settings, token, ghToken, lastCommit } =
|
||||
const { files, importedProjectZip } = useSelector(
|
||||
(state: RootState) => state.initalQuery,
|
||||
);
|
||||
const { start, send, setRuntimeIsInitialized, runtimeActive } = useSocket();
|
||||
const { settings, token, ghToken, repo, q, lastCommit } =
|
||||
useLoaderData<typeof clientLoader>();
|
||||
const fetcher = useFetcher();
|
||||
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
|
||||
|
||||
const secrets = React.useMemo(
|
||||
() => [ghToken, token].filter((secret) => secret !== null),
|
||||
[ghToken, token],
|
||||
);
|
||||
|
||||
// To avoid re-rendering the component when the user object changes, we memoize the user ID.
|
||||
// We use this to ensure the github token is valid before exporting it to the terminal.
|
||||
const userId = React.useMemo(() => {
|
||||
if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
|
||||
return null;
|
||||
}, [data?.user]);
|
||||
|
||||
const Terminal = React.useMemo(
|
||||
() => React.lazy(() => import("../components/terminal/Terminal")),
|
||||
[],
|
||||
);
|
||||
|
||||
const addIntialQueryToChat = (
|
||||
query: string,
|
||||
base64Files: string[],
|
||||
timestamp = new Date().toISOString(),
|
||||
) => {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: query,
|
||||
imageUrls: base64Files,
|
||||
timestamp,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const sendInitialQuery = (query: string, base64Files: string[]) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
send(createChatMessage(query, base64Files, timestamp));
|
||||
};
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
const initEvent = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
send(JSON.stringify(initEvent));
|
||||
|
||||
// display query in UI, but don't send it to the server
|
||||
if (q) addIntialQueryToChat(q, files);
|
||||
}, [settings]);
|
||||
|
||||
const handleMessage = React.useCallback(
|
||||
(message: MessageEvent<WebSocket.Data>) => {
|
||||
// set token received from the server
|
||||
const parsed = JSON.parse(message.data.toString());
|
||||
if ("token" in parsed) {
|
||||
fetcher.submit({ token: parsed.token }, { method: "post" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServerError(parsed)) {
|
||||
if (parsed.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
fetcher.submit({}, { method: "POST", action: "/end-session" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof parsed.error === "string") {
|
||||
toast.error(parsed.error);
|
||||
} else {
|
||||
toast.error(parsed.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (isErrorObservation(parsed)) {
|
||||
dispatch(
|
||||
addErrorMessage({
|
||||
id: parsed.extras?.error_id,
|
||||
message: parsed.message,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
handleAssistantMessage(message.data.toString());
|
||||
|
||||
// handle first time connection
|
||||
if (
|
||||
isAgentStateChange(parsed) &&
|
||||
parsed.extras.agent_state === AgentState.INIT
|
||||
) {
|
||||
setRuntimeIsInitialized();
|
||||
|
||||
// handle new session
|
||||
if (!token) {
|
||||
let additionalInfo = "";
|
||||
if (ghToken && repo) {
|
||||
send(getCloneRepoCommand(ghToken, repo));
|
||||
additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
|
||||
dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
|
||||
}
|
||||
// if there's an uploaded project zip, add it to the chat
|
||||
else if (importedProjectZip) {
|
||||
additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
|
||||
}
|
||||
|
||||
if (q) {
|
||||
if (additionalInfo) {
|
||||
sendInitialQuery(`${q}\n\n[${additionalInfo}]`, files);
|
||||
} else {
|
||||
sendInitialQuery(q, files);
|
||||
}
|
||||
dispatch(clearFiles()); // reset selected files
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[token, ghToken, repo, q, files],
|
||||
);
|
||||
|
||||
const startSocketConnection = React.useCallback(() => {
|
||||
start({
|
||||
token,
|
||||
onOpen: handleOpen,
|
||||
onMessage: handleMessage,
|
||||
});
|
||||
}, [token, handleOpen, handleMessage]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
// clear and restart the socket connection
|
||||
dispatch(clearMessages());
|
||||
dispatch(clearTerminal());
|
||||
dispatch(clearJupyter());
|
||||
dispatch(clearInitialQuery()); // Clear initial query when navigating to /app
|
||||
startSocketConnection();
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (runtimeActive && userId && ghToken) {
|
||||
// Export if the user valid, this could happen mid-session so it is handled here
|
||||
send(getGitHubTokenCommand(ghToken));
|
||||
}
|
||||
}, [userId, ghToken, runtimeActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (runtimeActive && importedProjectZip) {
|
||||
// upload files action
|
||||
try {
|
||||
const blob = base64ToBlob(importedProjectZip);
|
||||
const file = new File([blob], "imported-project.zip", {
|
||||
type: blob.type,
|
||||
});
|
||||
await OpenHands.uploadFiles([file]);
|
||||
dispatch(setImportedProjectZip(null));
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload project files.");
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [runtimeActive, importedProjectZip]);
|
||||
|
||||
const {
|
||||
isOpen: securityModalIsOpen,
|
||||
onOpen: onSecurityModalOpen,
|
||||
@@ -101,62 +291,53 @@ function App() {
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<WsClientProvider
|
||||
enabled
|
||||
token={token}
|
||||
ghToken={ghToken}
|
||||
settings={settings}
|
||||
>
|
||||
<EventHandler>
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto gap-3">
|
||||
<Container className="w-[390px] max-h-full relative">
|
||||
<ChatInterface />
|
||||
</Container>
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto gap-3">
|
||||
<Container className="w-[390px] max-h-full relative">
|
||||
<ChatInterface />
|
||||
</Container>
|
||||
|
||||
<div className="flex flex-col grow gap-3">
|
||||
<Container
|
||||
className="h-2/3"
|
||||
labels={[
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: "Browser",
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
isBeta: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<Container className="h-1/3 overflow-scroll" label="Terminal">
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[60px]">
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings.SECURITY_ANALYZER}
|
||||
lastCommitData={lastCommit}
|
||||
/>
|
||||
</div>
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
<div className="flex flex-col grow gap-3">
|
||||
<Container
|
||||
className="h-2/3"
|
||||
labels={[
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: "Browser",
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
isBeta: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<Container className="h-1/3 overflow-scroll" label="Terminal">
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
</div>
|
||||
</EventHandler>
|
||||
</WsClientProvider>
|
||||
</div>
|
||||
|
||||
<div className="h-[60px]">
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings.SECURITY_ANALYZER}
|
||||
lastCommitData={lastCommit}
|
||||
/>
|
||||
</div>
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ import { DangerModal } from "#/components/modals/confirmation-modals/danger-moda
|
||||
import { LoadingSpinner } from "#/components/modals/LoadingProject";
|
||||
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
|
||||
import { UserActions } from "#/components/user-actions";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import i18n from "#/i18n";
|
||||
import { getSettings, settingsAreUpToDate } from "#/services/settings";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import NewProjectIcon from "#/icons/new-project.svg?react";
|
||||
import DocsIcon from "#/icons/docs.svg?react";
|
||||
import NewProjectIcon from "#/assets/new-project.svg?react";
|
||||
import DocsIcon from "#/assets/docs.svg?react";
|
||||
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
|
||||
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
|
||||
import { WaitlistModal } from "#/components/waitlist-modal";
|
||||
@@ -134,6 +135,7 @@ type SettingsFormData = {
|
||||
};
|
||||
|
||||
export default function MainApp() {
|
||||
const { stop, isConnected } = useSocket();
|
||||
const navigation = useNavigation();
|
||||
const location = useLocation();
|
||||
const {
|
||||
@@ -171,8 +173,6 @@ export default function MainApp() {
|
||||
company: user.company,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
user: user.login,
|
||||
mode: window.__APP_MODE__ || "oss",
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
@@ -202,6 +202,14 @@ export default function MainApp() {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (location.pathname === "/") {
|
||||
// If the user is on the home page, we should stop the socket connection.
|
||||
// This is relevant when the user redirects here for whatever reason.
|
||||
if (isConnected) stop();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleUserLogout = () => {
|
||||
logoutFetcher.submit(
|
||||
{},
|
||||
@@ -305,9 +313,11 @@ export default function MainApp() {
|
||||
<p className="text-xs text-[#A3A3A3]">
|
||||
To continue, connect an OpenAI, Anthropic, or other LLM account
|
||||
</p>
|
||||
<p className="text-xs text-danger">
|
||||
Changing settings during an active session will end the session
|
||||
</p>
|
||||
{isConnected && (
|
||||
<p className="text-xs text-danger">
|
||||
Changing settings during an active session will end the session
|
||||
</p>
|
||||
)}
|
||||
<SettingsForm
|
||||
settings={settings}
|
||||
models={settingsFormData.models}
|
||||
|
||||
@@ -12,11 +12,8 @@ import {
|
||||
import { setCurStatusMessage } from "#/state/statusSlice";
|
||||
import store from "#/store";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import {
|
||||
ActionMessage,
|
||||
ObservationMessage,
|
||||
StatusMessage,
|
||||
} from "#/types/Message";
|
||||
import { ActionMessage, StatusMessage } from "#/types/Message";
|
||||
import { SocketMessage } from "#/types/ResponseType";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
|
||||
const messageActions = {
|
||||
@@ -141,14 +138,22 @@ export function handleStatusMessage(message: StatusMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAssistantMessage(message: Record<string, unknown>) {
|
||||
if (message.action) {
|
||||
handleActionMessage(message as unknown as ActionMessage);
|
||||
} else if (message.observation) {
|
||||
handleObservationMessage(message as unknown as ObservationMessage);
|
||||
} else if (message.status_update) {
|
||||
handleStatusMessage(message as unknown as StatusMessage);
|
||||
export function handleAssistantMessage(data: string | SocketMessage) {
|
||||
let socketMessage: SocketMessage;
|
||||
|
||||
if (typeof data === "string") {
|
||||
socketMessage = JSON.parse(data) as SocketMessage;
|
||||
} else {
|
||||
console.error("Unknown message type", message);
|
||||
socketMessage = data;
|
||||
}
|
||||
|
||||
if ("action" in socketMessage) {
|
||||
handleActionMessage(socketMessage);
|
||||
} else if ("observation" in socketMessage) {
|
||||
handleObservationMessage(socketMessage);
|
||||
} else if ("status_update" in socketMessage) {
|
||||
handleStatusMessage(socketMessage);
|
||||
} else {
|
||||
console.error("Unknown message type", socketMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ActionType from "#/types/ActionType";
|
||||
import AgentState from "#/types/AgentState";
|
||||
|
||||
export const generateAgentStateChangeEvent = (state: AgentState) => ({
|
||||
action: ActionType.CHANGE_AGENT_STATE,
|
||||
args: { agent_state: state },
|
||||
});
|
||||
export const generateAgentStateChangeEvent = (state: AgentState) =>
|
||||
JSON.stringify({
|
||||
action: ActionType.CHANGE_AGENT_STATE,
|
||||
args: { agent_state: state },
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@ import ActionType from "#/types/ActionType";
|
||||
|
||||
export function createChatMessage(
|
||||
message: string,
|
||||
image_urls: string[],
|
||||
images_urls: string[],
|
||||
timestamp: string,
|
||||
) {
|
||||
const event = {
|
||||
action: ActionType.MESSAGE,
|
||||
args: { content: message, image_urls, timestamp },
|
||||
args: { content: message, images_urls, timestamp },
|
||||
};
|
||||
return event;
|
||||
return JSON.stringify(event);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import ActionType from "#/types/ActionType";
|
||||
|
||||
export function getTerminalCommand(command: string, hidden: boolean = false) {
|
||||
const event = { action: ActionType.RUN, args: { command, hidden } };
|
||||
return event;
|
||||
return JSON.stringify(event);
|
||||
}
|
||||
|
||||
export function getGitHubTokenCommand(gitHubToken: string) {
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface UserMessageAction extends OpenHandsActionEvent<"message"> {
|
||||
source: "user";
|
||||
args: {
|
||||
content: string;
|
||||
image_urls: string[];
|
||||
images_urls: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface AssistantMessageAction
|
||||
source: "agent";
|
||||
args: {
|
||||
content: string;
|
||||
image_urls: string[] | null;
|
||||
images_urls: string[] | null;
|
||||
wait_for_response: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ interface LocalUserMessageAction {
|
||||
action: "message";
|
||||
args: {
|
||||
content: string;
|
||||
image_urls: string[];
|
||||
images_urls: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import store from "#/store";
|
||||
import { initialState as browserInitialState } from "#/state/browserSlice";
|
||||
|
||||
/**
|
||||
* Clear the session data from the local storage and reset relevant Redux state
|
||||
* Clear the session data from the local storage. This will remove the token and repo
|
||||
*/
|
||||
export const clearSession = () => {
|
||||
// Clear local storage
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("repo");
|
||||
|
||||
// Reset browser state to initial values
|
||||
store.dispatch({
|
||||
type: "browser/setUrl",
|
||||
payload: browserInitialState.url,
|
||||
});
|
||||
store.dispatch({
|
||||
type: "browser/setScreenshotSrc",
|
||||
payload: browserInitialState.screenshotSrc,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,14 +13,14 @@ const KEY_2 = "Auto-merge Dependabot PRs";
|
||||
const VALUE_2 = `Please add a GitHub action to this repository which automatically merges pull requests from Dependabot so long as the tests are passing.`;
|
||||
|
||||
const KEY_3 = "Fix up my README";
|
||||
const VALUE_3 = `Please look at the README and make the following improvements, if they make sense:
|
||||
const VALUE_3 = `"Please look at the README and make the following improvements, if they make sense:
|
||||
* correct any typos that you find
|
||||
* add missing language annotations on codeblocks
|
||||
* if there are references to other files or other sections of the README, turn them into links
|
||||
* make sure the readme has an h1 title towards the top
|
||||
* make sure any existing sections in the readme are appropriately separated with headings
|
||||
|
||||
If there are no obvious ways to improve the README, make at least one small change to make the wording clearer or friendlier`;
|
||||
If there are no obvious ways to improve the README, make at least one small change to make the wording clearer or friendlier"`;
|
||||
|
||||
const KEY_4 = "Clean up my dependencies";
|
||||
const VALUE_4 = `Examine the dependencies of the current codebase. Make sure you can run the code and any tests.
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
style: {
|
||||
background: "#ef4444",
|
||||
color: "#fff",
|
||||
lineBreak: "anywhere",
|
||||
},
|
||||
iconTheme: {
|
||||
primary: "#ef4444",
|
||||
@@ -18,20 +19,25 @@ export default {
|
||||
});
|
||||
idMap.set(id, toastId);
|
||||
},
|
||||
success: (id: string, msg: string, duration: number = 4000) => {
|
||||
if (idMap.has(id)) return; // prevent duplicate toast
|
||||
const toastId = toast.success(msg, {
|
||||
duration,
|
||||
style: {
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
},
|
||||
iconTheme: {
|
||||
primary: "#333",
|
||||
secondary: "#fff",
|
||||
},
|
||||
});
|
||||
idMap.set(id, toastId);
|
||||
success: (id: string, msg: string) => {
|
||||
const toastId = idMap.get(id);
|
||||
if (toastId === undefined) return;
|
||||
if (toastId) {
|
||||
toast.success(msg, {
|
||||
id: toastId,
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
lineBreak: "anywhere",
|
||||
},
|
||||
iconTheme: {
|
||||
primary: "#333",
|
||||
secondary: "#fff",
|
||||
},
|
||||
});
|
||||
}
|
||||
idMap.delete(id);
|
||||
},
|
||||
settingsChanged: (msg: string) => {
|
||||
toast(msg, {
|
||||
@@ -42,6 +48,7 @@ export default {
|
||||
style: {
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
lineBreak: "anywhere",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export const VERIFIED_ANTHROPIC_MODELS = [
|
||||
"claude-2",
|
||||
"claude-2.1",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { RenderOptions, render } from "@testing-library/react";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { SocketProvider } from "#/context/socket";
|
||||
|
||||
const setupStore = (preloadedState?: Partial<RootState>): AppStore =>
|
||||
configureStore({
|
||||
@@ -35,7 +35,7 @@ export function renderWithProviders(
|
||||
function Wrapper({ children }: PropsWithChildren<object>): JSX.Element {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<WsClientProvider enabled={true} token={null} ghToken={null} settings={null}>{children}</WsClientProvider>
|
||||
<SocketProvider>{children}</SocketProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
744
get-docker.sh
Normal file
@@ -0,0 +1,744 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Docker Engine for Linux installation script.
|
||||
#
|
||||
# This script is intended as a convenient way to configure docker's package
|
||||
# repositories and to install Docker Engine, This script is not recommended
|
||||
# for production environments. Before running this script, make yourself familiar
|
||||
# with potential risks and limitations, and refer to the installation manual
|
||||
# at https://docs.docker.com/engine/install/ for alternative installation methods.
|
||||
#
|
||||
# The script:
|
||||
#
|
||||
# - Requires `root` or `sudo` privileges to run.
|
||||
# - Attempts to detect your Linux distribution and version and configure your
|
||||
# package management system for you.
|
||||
# - Doesn't allow you to customize most installation parameters.
|
||||
# - Installs dependencies and recommendations without asking for confirmation.
|
||||
# - Installs the latest stable release (by default) of Docker CLI, Docker Engine,
|
||||
# Docker Buildx, Docker Compose, containerd, and runc. When using this script
|
||||
# to provision a machine, this may result in unexpected major version upgrades
|
||||
# of these packages. Always test upgrades in a test environment before
|
||||
# deploying to your production systems.
|
||||
# - Isn't designed to upgrade an existing Docker installation. When using the
|
||||
# script to update an existing installation, dependencies may not be updated
|
||||
# to the expected version, resulting in outdated versions.
|
||||
#
|
||||
# Source code is available at https://github.com/docker/docker-install/
|
||||
#
|
||||
# Usage
|
||||
# ==============================================================================
|
||||
#
|
||||
# To install the latest stable versions of Docker CLI, Docker Engine, and their
|
||||
# dependencies:
|
||||
#
|
||||
# 1. download the script
|
||||
#
|
||||
# $ curl -fsSL https://get.docker.com -o install-docker.sh
|
||||
#
|
||||
# 2. verify the script's content
|
||||
#
|
||||
# $ cat install-docker.sh
|
||||
#
|
||||
# 3. run the script with --dry-run to verify the steps it executes
|
||||
#
|
||||
# $ sh install-docker.sh --dry-run
|
||||
#
|
||||
# 4. run the script either as root, or using sudo to perform the installation.
|
||||
#
|
||||
# $ sudo sh install-docker.sh
|
||||
#
|
||||
# Command-line options
|
||||
# ==============================================================================
|
||||
#
|
||||
# --version <VERSION>
|
||||
# Use the --version option to install a specific version, for example:
|
||||
#
|
||||
# $ sudo sh install-docker.sh --version 23.0
|
||||
#
|
||||
# --channel <stable|test>
|
||||
#
|
||||
# Use the --channel option to install from an alternative installation channel.
|
||||
# The following example installs the latest versions from the "test" channel,
|
||||
# which includes pre-releases (alpha, beta, rc):
|
||||
#
|
||||
# $ sudo sh install-docker.sh --channel test
|
||||
#
|
||||
# Alternatively, use the script at https://test.docker.com, which uses the test
|
||||
# channel as default.
|
||||
#
|
||||
# --mirror <Aliyun|AzureChinaCloud>
|
||||
#
|
||||
# Use the --mirror option to install from a mirror supported by this script.
|
||||
# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and
|
||||
# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example:
|
||||
#
|
||||
# $ sudo sh install-docker.sh --mirror AzureChinaCloud
|
||||
#
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
# Git commit from https://github.com/docker/docker-install when
|
||||
# the script was uploaded (Should only be modified by upload job):
|
||||
SCRIPT_COMMIT_SHA="711a0d41213afabc30b963f82c56e1442a3efe1c"
|
||||
|
||||
# strip "v" prefix if present
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
# The channel to install from:
|
||||
# * stable
|
||||
# * test
|
||||
DEFAULT_CHANNEL_VALUE="stable"
|
||||
if [ -z "$CHANNEL" ]; then
|
||||
CHANNEL=$DEFAULT_CHANNEL_VALUE
|
||||
fi
|
||||
|
||||
DEFAULT_DOWNLOAD_URL="https://download.docker.com"
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL
|
||||
fi
|
||||
|
||||
DEFAULT_REPO_FILE="docker-ce.repo"
|
||||
if [ -z "$REPO_FILE" ]; then
|
||||
REPO_FILE="$DEFAULT_REPO_FILE"
|
||||
fi
|
||||
|
||||
mirror=''
|
||||
DRY_RUN=${DRY_RUN:-}
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--channel)
|
||||
CHANNEL="$2"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
;;
|
||||
--mirror)
|
||||
mirror="$2"
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2#v}"
|
||||
shift
|
||||
;;
|
||||
--*)
|
||||
echo "Illegal option $1"
|
||||
;;
|
||||
esac
|
||||
shift $(( $# > 0 ? 1 : 0 ))
|
||||
done
|
||||
|
||||
case "$mirror" in
|
||||
Aliyun)
|
||||
DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce"
|
||||
;;
|
||||
AzureChinaCloud)
|
||||
DOWNLOAD_URL="https://mirror.azure.cn/docker-ce"
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
>&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$CHANNEL" in
|
||||
stable|test)
|
||||
;;
|
||||
*)
|
||||
>&2 echo "unknown CHANNEL '$CHANNEL': use either stable or test."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# version_gte checks if the version specified in $VERSION is at least the given
|
||||
# SemVer (Maj.Minor[.Patch]), or CalVer (YY.MM) version.It returns 0 (success)
|
||||
# if $VERSION is either unset (=latest) or newer or equal than the specified
|
||||
# version, or returns 1 (fail) otherwise.
|
||||
#
|
||||
# examples:
|
||||
#
|
||||
# VERSION=23.0
|
||||
# version_gte 23.0 // 0 (success)
|
||||
# version_gte 20.10 // 0 (success)
|
||||
# version_gte 19.03 // 0 (success)
|
||||
# version_gte 26.1 // 1 (fail)
|
||||
version_gte() {
|
||||
if [ -z "$VERSION" ]; then
|
||||
return 0
|
||||
fi
|
||||
version_compare "$VERSION" "$1"
|
||||
}
|
||||
|
||||
# version_compare compares two version strings (either SemVer (Major.Minor.Path),
|
||||
# or CalVer (YY.MM) version strings. It returns 0 (success) if version A is newer
|
||||
# or equal than version B, or 1 (fail) otherwise. Patch releases and pre-release
|
||||
# (-alpha/-beta) are not taken into account
|
||||
#
|
||||
# examples:
|
||||
#
|
||||
# version_compare 23.0.0 20.10 // 0 (success)
|
||||
# version_compare 23.0 20.10 // 0 (success)
|
||||
# version_compare 20.10 19.03 // 0 (success)
|
||||
# version_compare 20.10 20.10 // 0 (success)
|
||||
# version_compare 19.03 20.10 // 1 (fail)
|
||||
version_compare() (
|
||||
set +x
|
||||
|
||||
yy_a="$(echo "$1" | cut -d'.' -f1)"
|
||||
yy_b="$(echo "$2" | cut -d'.' -f1)"
|
||||
if [ "$yy_a" -lt "$yy_b" ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$yy_a" -gt "$yy_b" ]; then
|
||||
return 0
|
||||
fi
|
||||
mm_a="$(echo "$1" | cut -d'.' -f2)"
|
||||
mm_b="$(echo "$2" | cut -d'.' -f2)"
|
||||
|
||||
# trim leading zeros to accommodate CalVer
|
||||
mm_a="${mm_a#0}"
|
||||
mm_b="${mm_b#0}"
|
||||
|
||||
if [ "${mm_a:-0}" -lt "${mm_b:-0}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
)
|
||||
|
||||
is_dry_run() {
|
||||
if [ -z "$DRY_RUN" ]; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
is_wsl() {
|
||||
case "$(uname -r)" in
|
||||
*microsoft* ) true ;; # WSL 2
|
||||
*Microsoft* ) true ;; # WSL 1
|
||||
* ) false;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_darwin() {
|
||||
case "$(uname -s)" in
|
||||
*darwin* ) true ;;
|
||||
*Darwin* ) true ;;
|
||||
* ) false;;
|
||||
esac
|
||||
}
|
||||
|
||||
deprecation_notice() {
|
||||
distro=$1
|
||||
distro_version=$2
|
||||
echo
|
||||
printf "\033[91;1mDEPRECATION WARNING\033[0m\n"
|
||||
printf " This Linux distribution (\033[1m%s %s\033[0m) reached end-of-life and is no longer supported by this script.\n" "$distro" "$distro_version"
|
||||
echo " No updates or security fixes will be released for this distribution, and users are recommended"
|
||||
echo " to upgrade to a currently maintained version of $distro."
|
||||
echo
|
||||
printf "Press \033[1mCtrl+C\033[0m now to abort this script, or wait for the installation to continue."
|
||||
echo
|
||||
sleep 10
|
||||
}
|
||||
|
||||
get_distribution() {
|
||||
lsb_dist=""
|
||||
# Every system that we officially support has /etc/os-release
|
||||
if [ -r /etc/os-release ]; then
|
||||
lsb_dist="$(. /etc/os-release && echo "$ID")"
|
||||
fi
|
||||
# Returning an empty string here should be alright since the
|
||||
# case statements don't act unless you provide an actual value
|
||||
echo "$lsb_dist"
|
||||
}
|
||||
|
||||
echo_docker_as_nonroot() {
|
||||
if is_dry_run; then
|
||||
return
|
||||
fi
|
||||
if command_exists docker && [ -e /var/run/docker.sock ]; then
|
||||
(
|
||||
set -x
|
||||
$sh_c 'docker version'
|
||||
) || true
|
||||
fi
|
||||
|
||||
# intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output
|
||||
echo
|
||||
echo "================================================================================"
|
||||
echo
|
||||
if version_gte "20.10"; then
|
||||
echo "To run Docker as a non-privileged user, consider setting up the"
|
||||
echo "Docker daemon in rootless mode for your user:"
|
||||
echo
|
||||
echo " dockerd-rootless-setuptool.sh install"
|
||||
echo
|
||||
echo "Visit https://docs.docker.com/go/rootless/ to learn about rootless mode."
|
||||
echo
|
||||
fi
|
||||
echo
|
||||
echo "To run the Docker daemon as a fully privileged service, but granting non-root"
|
||||
echo "users access, refer to https://docs.docker.com/go/daemon-access/"
|
||||
echo
|
||||
echo "WARNING: Access to the remote API on a privileged Docker daemon is equivalent"
|
||||
echo " to root access on the host. Refer to the 'Docker daemon attack surface'"
|
||||
echo " documentation for details: https://docs.docker.com/go/attack-surface/"
|
||||
echo
|
||||
echo "================================================================================"
|
||||
echo
|
||||
}
|
||||
|
||||
# Check if this is a forked Linux distro
|
||||
check_forked() {
|
||||
|
||||
# Check for lsb_release command existence, it usually exists in forked distros
|
||||
if command_exists lsb_release; then
|
||||
# Check if the `-u` option is supported
|
||||
set +e
|
||||
lsb_release -a -u > /dev/null 2>&1
|
||||
lsb_release_exit_code=$?
|
||||
set -e
|
||||
|
||||
# Check if the command has exited successfully, it means we're in a forked distro
|
||||
if [ "$lsb_release_exit_code" = "0" ]; then
|
||||
# Print info about current distro
|
||||
cat <<-EOF
|
||||
You're using '$lsb_dist' version '$dist_version'.
|
||||
EOF
|
||||
|
||||
# Get the upstream release info
|
||||
lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||
dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||
|
||||
# Print info about upstream distro
|
||||
cat <<-EOF
|
||||
Upstream release is '$lsb_dist' version '$dist_version'.
|
||||
EOF
|
||||
else
|
||||
if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then
|
||||
if [ "$lsb_dist" = "osmc" ]; then
|
||||
# OSMC runs Raspbian
|
||||
lsb_dist=raspbian
|
||||
else
|
||||
# We're Debian and don't even know it!
|
||||
lsb_dist=debian
|
||||
fi
|
||||
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||
case "$dist_version" in
|
||||
12)
|
||||
dist_version="bookworm"
|
||||
;;
|
||||
11)
|
||||
dist_version="bullseye"
|
||||
;;
|
||||
10)
|
||||
dist_version="buster"
|
||||
;;
|
||||
9)
|
||||
dist_version="stretch"
|
||||
;;
|
||||
8)
|
||||
dist_version="jessie"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
do_install() {
|
||||
echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA"
|
||||
|
||||
if command_exists docker; then
|
||||
cat >&2 <<-'EOF'
|
||||
Warning: the "docker" command appears to already exist on this system.
|
||||
|
||||
If you already have Docker installed, this script can cause trouble, which is
|
||||
why we're displaying this warning and provide the opportunity to cancel the
|
||||
installation.
|
||||
|
||||
If you installed the current Docker package using this script and are using it
|
||||
again to update Docker, you can safely ignore this message.
|
||||
|
||||
You may press Ctrl+C now to abort this script.
|
||||
EOF
|
||||
( set -x; sleep 20 )
|
||||
fi
|
||||
|
||||
user="$(id -un 2>/dev/null || true)"
|
||||
|
||||
sh_c='sh -c'
|
||||
if [ "$user" != 'root' ]; then
|
||||
if command_exists sudo; then
|
||||
sh_c='sudo -E sh -c'
|
||||
elif command_exists su; then
|
||||
sh_c='su -c'
|
||||
else
|
||||
cat >&2 <<-'EOF'
|
||||
Error: this installer needs the ability to run commands as root.
|
||||
We are unable to find either "sudo" or "su" available to make this happen.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if is_dry_run; then
|
||||
sh_c="echo"
|
||||
fi
|
||||
|
||||
# perform some very rudimentary platform detection
|
||||
lsb_dist=$( get_distribution )
|
||||
lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if is_wsl; then
|
||||
echo
|
||||
echo "WSL DETECTED: We recommend using Docker Desktop for Windows."
|
||||
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop/"
|
||||
echo
|
||||
cat >&2 <<-'EOF'
|
||||
|
||||
You may press Ctrl+C now to abort this script.
|
||||
EOF
|
||||
( set -x; sleep 20 )
|
||||
fi
|
||||
|
||||
case "$lsb_dist" in
|
||||
|
||||
ubuntu)
|
||||
if command_exists lsb_release; then
|
||||
dist_version="$(lsb_release --codename | cut -f2)"
|
||||
fi
|
||||
if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then
|
||||
dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")"
|
||||
fi
|
||||
;;
|
||||
|
||||
debian|raspbian)
|
||||
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||
case "$dist_version" in
|
||||
12)
|
||||
dist_version="bookworm"
|
||||
;;
|
||||
11)
|
||||
dist_version="bullseye"
|
||||
;;
|
||||
10)
|
||||
dist_version="buster"
|
||||
;;
|
||||
9)
|
||||
dist_version="stretch"
|
||||
;;
|
||||
8)
|
||||
dist_version="jessie"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
centos|rhel)
|
||||
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
if command_exists lsb_release; then
|
||||
dist_version="$(lsb_release --release | cut -f2)"
|
||||
fi
|
||||
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||
fi
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
# Check if this is a forked Linux distro
|
||||
check_forked
|
||||
|
||||
# Print deprecation warnings for distro versions that recently reached EOL,
|
||||
# but may still be commonly used (especially LTS versions).
|
||||
case "$lsb_dist.$dist_version" in
|
||||
centos.8|centos.7|rhel.7)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
debian.buster|debian.stretch|debian.jessie)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
raspbian.buster|raspbian.stretch|raspbian.jessie)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
ubuntu.bionic|ubuntu.xenial|ubuntu.trusty)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
ubuntu.mantic|ubuntu.lunar|ubuntu.kinetic|ubuntu.impish|ubuntu.hirsute|ubuntu.groovy|ubuntu.eoan|ubuntu.disco|ubuntu.cosmic)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
fedora.*)
|
||||
if [ "$dist_version" -lt 39 ]; then
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Run setup for each distro accordingly
|
||||
case "$lsb_dist" in
|
||||
ubuntu|debian|raspbian)
|
||||
pre_reqs="ca-certificates curl"
|
||||
apt_repo="deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL"
|
||||
(
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c 'apt-get -qq update >/dev/null'
|
||||
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pre_reqs >/dev/null"
|
||||
$sh_c 'install -m 0755 -d /etc/apt/keyrings'
|
||||
$sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" -o /etc/apt/keyrings/docker.asc"
|
||||
$sh_c "chmod a+r /etc/apt/keyrings/docker.asc"
|
||||
$sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list"
|
||||
$sh_c 'apt-get -qq update >/dev/null'
|
||||
)
|
||||
pkg_version=""
|
||||
if [ -n "$VERSION" ]; then
|
||||
if is_dry_run; then
|
||||
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||
else
|
||||
# Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel
|
||||
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')"
|
||||
search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||
pkg_version="$($sh_c "$search_command")"
|
||||
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||
echo "INFO: $search_command"
|
||||
if [ -z "$pkg_version" ]; then
|
||||
echo
|
||||
echo "ERROR: '$VERSION' not found amongst apt-cache madison results"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
if version_gte "18.09"; then
|
||||
search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||
echo "INFO: $search_command"
|
||||
cli_pkg_version="=$($sh_c "$search_command")"
|
||||
fi
|
||||
pkg_version="=$pkg_version"
|
||||
fi
|
||||
fi
|
||||
(
|
||||
pkgs="docker-ce${pkg_version%=}"
|
||||
if version_gte "18.09"; then
|
||||
# older versions didn't ship the cli and containerd as separate packages
|
||||
pkgs="$pkgs docker-ce-cli${cli_pkg_version%=} containerd.io"
|
||||
fi
|
||||
if version_gte "20.10"; then
|
||||
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||
fi
|
||||
if version_gte "23.0"; then
|
||||
pkgs="$pkgs docker-buildx-plugin"
|
||||
fi
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pkgs >/dev/null"
|
||||
)
|
||||
echo_docker_as_nonroot
|
||||
exit 0
|
||||
;;
|
||||
centos|fedora|rhel)
|
||||
repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
|
||||
(
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
if command_exists dnf5; then
|
||||
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||
$sh_c "dnf5 config-manager addrepo --save-filename=docker-ce.repo --from-repofile='$repo_file_url'"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "dnf5 config-manager setopt \"docker-ce-*.enabled=0\""
|
||||
$sh_c "dnf5 config-manager setopt \"docker-ce-$CHANNEL.enabled=1\""
|
||||
fi
|
||||
$sh_c "dnf makecache"
|
||||
elif command_exists dnf; then
|
||||
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||
$sh_c "dnf config-manager --add-repo $repo_file_url"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "dnf config-manager --set-disabled \"docker-ce-*\""
|
||||
$sh_c "dnf config-manager --set-enabled \"docker-ce-$CHANNEL\""
|
||||
fi
|
||||
$sh_c "dnf makecache"
|
||||
else
|
||||
$sh_c "yum -y -q install yum-utils"
|
||||
$sh_c "yum-config-manager --add-repo $repo_file_url"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "yum-config-manager --disable \"docker-ce-*\""
|
||||
$sh_c "yum-config-manager --enable \"docker-ce-$CHANNEL\""
|
||||
fi
|
||||
$sh_c "yum makecache"
|
||||
fi
|
||||
)
|
||||
pkg_version=""
|
||||
if command_exists dnf; then
|
||||
pkg_manager="dnf"
|
||||
pkg_manager_flags="-y -q --best"
|
||||
else
|
||||
pkg_manager="yum"
|
||||
pkg_manager_flags="-y -q"
|
||||
fi
|
||||
if [ -n "$VERSION" ]; then
|
||||
if is_dry_run; then
|
||||
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||
else
|
||||
if [ "$lsb_dist" = "fedora" ]; then
|
||||
pkg_suffix="fc$dist_version"
|
||||
else
|
||||
pkg_suffix="el"
|
||||
fi
|
||||
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g').*$pkg_suffix"
|
||||
search_command="$pkg_manager list --showduplicates docker-ce | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||
pkg_version="$($sh_c "$search_command")"
|
||||
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||
echo "INFO: $search_command"
|
||||
if [ -z "$pkg_version" ]; then
|
||||
echo
|
||||
echo "ERROR: '$VERSION' not found amongst $pkg_manager list results"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
if version_gte "18.09"; then
|
||||
# older versions don't support a cli package
|
||||
search_command="$pkg_manager list --showduplicates docker-ce-cli | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||
cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)"
|
||||
fi
|
||||
# Cut out the epoch and prefix with a '-'
|
||||
pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)"
|
||||
fi
|
||||
fi
|
||||
(
|
||||
pkgs="docker-ce$pkg_version"
|
||||
if version_gte "18.09"; then
|
||||
# older versions didn't ship the cli and containerd as separate packages
|
||||
if [ -n "$cli_pkg_version" ]; then
|
||||
pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
|
||||
else
|
||||
pkgs="$pkgs docker-ce-cli containerd.io"
|
||||
fi
|
||||
fi
|
||||
if version_gte "20.10"; then
|
||||
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||
fi
|
||||
if version_gte "23.0"; then
|
||||
pkgs="$pkgs docker-buildx-plugin"
|
||||
fi
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "$pkg_manager $pkg_manager_flags install $pkgs"
|
||||
)
|
||||
echo_docker_as_nonroot
|
||||
exit 0
|
||||
;;
|
||||
sles)
|
||||
if [ "$(uname -m)" != "s390x" ]; then
|
||||
echo "Packages for SLES are currently only available for s390x"
|
||||
exit 1
|
||||
fi
|
||||
repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
|
||||
pre_reqs="ca-certificates curl libseccomp2 awk"
|
||||
(
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "zypper install -y $pre_reqs"
|
||||
$sh_c "zypper addrepo $repo_file_url"
|
||||
if ! is_dry_run; then
|
||||
cat >&2 <<-'EOF'
|
||||
WARNING!!
|
||||
openSUSE repository (https://download.opensuse.org/repositories/security:/SELinux) will be enabled now.
|
||||
Do you wish to continue?
|
||||
You may press Ctrl+C now to abort this script.
|
||||
EOF
|
||||
( set -x; sleep 30 )
|
||||
fi
|
||||
opensuse_repo="https://download.opensuse.org/repositories/security:/SELinux/openSUSE_Factory/security:SELinux.repo"
|
||||
$sh_c "zypper addrepo $opensuse_repo"
|
||||
$sh_c "zypper --gpg-auto-import-keys refresh"
|
||||
$sh_c "zypper lr -d"
|
||||
)
|
||||
pkg_version=""
|
||||
if [ -n "$VERSION" ]; then
|
||||
if is_dry_run; then
|
||||
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||
else
|
||||
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g')"
|
||||
search_command="zypper search -s --match-exact 'docker-ce' | grep '$pkg_pattern' | tail -1 | awk '{print \$6}'"
|
||||
pkg_version="$($sh_c "$search_command")"
|
||||
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||
echo "INFO: $search_command"
|
||||
if [ -z "$pkg_version" ]; then
|
||||
echo
|
||||
echo "ERROR: '$VERSION' not found amongst zypper list results"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
search_command="zypper search -s --match-exact 'docker-ce-cli' | grep '$pkg_pattern' | tail -1 | awk '{print \$6}'"
|
||||
# It's okay for cli_pkg_version to be blank, since older versions don't support a cli package
|
||||
cli_pkg_version="$($sh_c "$search_command")"
|
||||
pkg_version="-$pkg_version"
|
||||
fi
|
||||
fi
|
||||
(
|
||||
pkgs="docker-ce$pkg_version"
|
||||
if version_gte "18.09"; then
|
||||
if [ -n "$cli_pkg_version" ]; then
|
||||
# older versions didn't ship the cli and containerd as separate packages
|
||||
pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
|
||||
else
|
||||
pkgs="$pkgs docker-ce-cli containerd.io"
|
||||
fi
|
||||
fi
|
||||
if version_gte "20.10"; then
|
||||
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||
fi
|
||||
if version_gte "23.0"; then
|
||||
pkgs="$pkgs docker-buildx-plugin"
|
||||
fi
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "zypper -q install -y $pkgs"
|
||||
)
|
||||
echo_docker_as_nonroot
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [ -z "$lsb_dist" ]; then
|
||||
if is_darwin; then
|
||||
echo
|
||||
echo "ERROR: Unsupported operating system 'macOS'"
|
||||
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
echo "ERROR: Unsupported distribution '$lsb_dist'"
|
||||
echo
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
}
|
||||
|
||||
# wrapped up in a function so that we have some protection against only getting
|
||||
# half the file during "curl | sh"
|
||||
do_install
|
||||
@@ -103,17 +103,15 @@ class CodeActAgent(Agent):
|
||||
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2)}'
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro') if self.config.use_microagents else None,
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro'),
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts', 'tools'),
|
||||
disabled_microagents=self.config.disabled_microagents,
|
||||
)
|
||||
else:
|
||||
self.action_parser = CodeActResponseParser()
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro') if self.config.use_microagents else None,
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro'),
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts', 'default'),
|
||||
agent_skills_docs=AgentSkillsRequirement.documentation,
|
||||
disabled_microagents=self.config.disabled_microagents,
|
||||
)
|
||||
|
||||
self.pending_actions: deque[Action] = deque()
|
||||
@@ -198,8 +196,8 @@ class CodeActAgent(Agent):
|
||||
elif isinstance(action, MessageAction):
|
||||
role = 'user' if action.source == 'user' else 'assistant'
|
||||
content = [TextContent(text=action.content or '')]
|
||||
if self.llm.vision_is_active() and action.image_urls:
|
||||
content.append(ImageContent(image_urls=action.image_urls))
|
||||
if self.llm.vision_is_active() and action.images_urls:
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
return [
|
||||
Message(
|
||||
role=role,
|
||||
|
||||
@@ -95,9 +95,9 @@ class CodeActSWEAgent(Agent):
|
||||
if (
|
||||
self.llm.vision_is_active()
|
||||
and isinstance(action, MessageAction)
|
||||
and action.image_urls
|
||||
and action.images_urls
|
||||
):
|
||||
content.append(ImageContent(image_urls=action.image_urls))
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
|
||||
return Message(
|
||||
role='user' if action.source == 'user' else 'assistant', content=content
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import os
|
||||
import traceback
|
||||
from typing import Callable, ClassVar, Type
|
||||
|
||||
@@ -42,7 +41,7 @@ from openhands.events.observation import (
|
||||
)
|
||||
from openhands.events.serialization.event import truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
from openhands.runtime.utils.shutdown_listener import should_continue
|
||||
|
||||
# note: RESUME is only available on web GUI
|
||||
TRAFFIC_CONTROL_REMINDER = (
|
||||
@@ -260,11 +259,7 @@ class AgentController:
|
||||
observation_to_print.content = truncate_content(
|
||||
observation_to_print.content, self.agent.llm.config.max_message_chars
|
||||
)
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
self.log(
|
||||
log_level, str(observation_to_print), extra={'msg_type': 'OBSERVATION'}
|
||||
)
|
||||
self.log('debug', str(observation_to_print), extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
if observation.llm_metrics is not None:
|
||||
self.agent.llm.metrics.merge(observation.llm_metrics)
|
||||
@@ -287,12 +282,8 @@ class AgentController:
|
||||
action (MessageAction): The message action to handle.
|
||||
"""
|
||||
if action.source == EventSource.USER:
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = (
|
||||
'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
)
|
||||
self.log(
|
||||
log_level,
|
||||
'debug',
|
||||
str(action),
|
||||
extra={'msg_type': 'ACTION', 'event_source': EventSource.USER},
|
||||
)
|
||||
@@ -506,9 +497,7 @@ class AgentController:
|
||||
|
||||
await self.update_state_after_step()
|
||||
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
self.log(log_level, str(action), extra={'msg_type': 'ACTION'})
|
||||
self.log('debug', str(action), extra={'msg_type': 'ACTION'})
|
||||
|
||||
async def _delegate_step(self):
|
||||
"""Executes a single step of the delegate agent."""
|
||||
@@ -674,7 +663,7 @@ class AgentController:
|
||||
# sanity check
|
||||
if start_id > end_id + 1:
|
||||
self.log(
|
||||
'warning',
|
||||
'debug',
|
||||
f'start_id {start_id} is greater than end_id + 1 ({end_id + 1}). History will be empty.',
|
||||
)
|
||||
self.state.history = []
|
||||
@@ -705,7 +694,7 @@ class AgentController:
|
||||
# Match with most recent unmatched delegate action
|
||||
if not delegate_action_ids:
|
||||
self.log(
|
||||
'warning',
|
||||
'error',
|
||||
f'Found AgentDelegateObservation without matching action at id={event.id}',
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -149,7 +149,7 @@ class State:
|
||||
for event in reversed(self.history):
|
||||
if isinstance(event, MessageAction) and event.source == 'user':
|
||||
last_user_message = event.content
|
||||
last_user_message_image_urls = event.image_urls
|
||||
last_user_message_image_urls = event.images_urls
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
if last_user_message is not None:
|
||||
return last_user_message, None
|
||||
|
||||
@@ -116,7 +116,6 @@ async def main():
|
||||
event_stream=event_stream,
|
||||
sid=sid,
|
||||
plugins=agent_cls.sandbox_plugins,
|
||||
headless_mode=True,
|
||||
)
|
||||
|
||||
controller = AgentController(
|
||||
|
||||
@@ -16,8 +16,6 @@ class AgentConfig:
|
||||
memory_enabled: Whether long-term memory (embeddings) is enabled.
|
||||
memory_max_threads: The maximum number of threads indexing at the same time for embeddings.
|
||||
llm_config: The name of the llm config to use. If specified, this will override global llm config.
|
||||
use_microagents: Whether to use microagents at all. Default is True.
|
||||
disabled_microagents: A list of microagents to disable. Default is None.
|
||||
"""
|
||||
|
||||
function_calling: bool = True
|
||||
@@ -28,8 +26,6 @@ class AgentConfig:
|
||||
memory_enabled: bool = False
|
||||
memory_max_threads: int = 3
|
||||
llm_config: str | None = None
|
||||
use_microagents: bool = True
|
||||
disabled_microagents: list[str] | None = None
|
||||
|
||||
def defaults_to_dict(self) -> dict:
|
||||
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
|
||||
|
||||
@@ -36,7 +36,7 @@ class SandboxConfig:
|
||||
|
||||
remote_runtime_api_url: str = 'http://localhost:8000'
|
||||
local_runtime_url: str = 'http://localhost'
|
||||
keep_runtime_alive: bool = True
|
||||
keep_remote_runtime_alive: bool = True
|
||||
api_key: str | None = None
|
||||
base_container_image: str = 'nikolaik/python-nodejs:python3.12-nodejs22' # default to nikolaik/python-nodejs:python3.12-nodejs22 for eventstream runtime
|
||||
runtime_container_image: str | None = None
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -46,29 +45,6 @@ LOG_COLORS: Mapping[str, ColorType] = {
|
||||
}
|
||||
|
||||
|
||||
class NoColorFormatter(logging.Formatter):
|
||||
"""Formatter for non-colored logging in files."""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
# Create a deep copy of the record to avoid modifying the original
|
||||
new_record: logging.LogRecord = copy.deepcopy(record)
|
||||
# Strip ANSI color codes from the message
|
||||
new_record.msg = strip_ansi(new_record.msg)
|
||||
|
||||
return super().format(new_record)
|
||||
|
||||
|
||||
def strip_ansi(s: str) -> str:
|
||||
"""
|
||||
Removes ANSI escape sequences from str, as defined by ECMA-048 in
|
||||
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf
|
||||
# https://github.com/ewen-lbh/python-strip-ansi/blob/master/strip_ansi/__init__.py
|
||||
"""
|
||||
pattern = re.compile(r'\x1B\[\d+(;\d+){0,2}m')
|
||||
stripped = pattern.sub('', s)
|
||||
return stripped
|
||||
|
||||
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
msg_type = record.__dict__.get('msg_type')
|
||||
@@ -94,7 +70,7 @@ class ColoredFormatter(logging.Formatter):
|
||||
return super().format(record)
|
||||
|
||||
|
||||
file_formatter = NoColorFormatter(
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s:%(levelname)s: %(filename)s:%(lineno)s - %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
)
|
||||
@@ -201,7 +177,7 @@ class SensitiveDataFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
def get_console_handler(log_level: int = logging.INFO, extra_info: str | None = None):
|
||||
def get_console_handler(log_level=logging.INFO, extra_info: str | None = None):
|
||||
"""Returns a console handler for logging."""
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(log_level)
|
||||
@@ -212,7 +188,7 @@ def get_console_handler(log_level: int = logging.INFO, extra_info: str | None =
|
||||
return console_handler
|
||||
|
||||
|
||||
def get_file_handler(log_dir: str, log_level: int = logging.INFO):
|
||||
def get_file_handler(log_dir, log_level=logging.INFO):
|
||||
"""Returns a file handler for logging."""
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
@@ -54,14 +54,11 @@ def read_task_from_stdin() -> str:
|
||||
def create_runtime(
|
||||
config: AppConfig,
|
||||
sid: str | None = None,
|
||||
headless_mode: bool = True,
|
||||
) -> Runtime:
|
||||
"""Create a runtime for the agent to run on.
|
||||
|
||||
config: The app config.
|
||||
sid: The session id.
|
||||
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
|
||||
where we don't want to have the VSCode UI open, so it defaults to True.
|
||||
"""
|
||||
# if sid is provided on the command line, use it as the name of the event stream
|
||||
# otherwise generate it on the basis of the configured jwt_secret
|
||||
@@ -83,7 +80,6 @@ def create_runtime(
|
||||
event_stream=event_stream,
|
||||
sid=session_id,
|
||||
plugins=agent_cls.sandbox_plugins,
|
||||
headless_mode=headless_mode,
|
||||
)
|
||||
|
||||
return runtime
|
||||
@@ -126,7 +122,7 @@ async def run_controller(
|
||||
sid = sid or generate_sid(config)
|
||||
|
||||
if runtime is None:
|
||||
runtime = create_runtime(config, sid=sid, headless_mode=headless_mode)
|
||||
runtime = create_runtime(config, sid=sid)
|
||||
await runtime.connect()
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
@@ -98,13 +98,6 @@ class Message(BaseModel):
|
||||
content.extend(d)
|
||||
|
||||
ret: dict = {'content': content, 'role': self.role}
|
||||
# pop content if it's empty
|
||||
if not content or (
|
||||
len(content) == 1
|
||||
and content[0]['type'] == 'text'
|
||||
and content[0]['text'] == ''
|
||||
):
|
||||
ret.pop('content')
|
||||
|
||||
if role_tool_with_prompt_caching:
|
||||
ret['cache_control'] = {'type': 'ephemeral'}
|
||||
|
||||
@@ -7,7 +7,7 @@ from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
@dataclass
|
||||
class MessageAction(Action):
|
||||
content: str
|
||||
image_urls: list[str] | None = None
|
||||
images_urls: list[str] | None = None
|
||||
wait_for_response: bool = False
|
||||
action: str = ActionType.MESSAGE
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
@@ -16,18 +16,10 @@ class MessageAction(Action):
|
||||
def message(self) -> str:
|
||||
return self.content
|
||||
|
||||
@property
|
||||
def images_urls(self):
|
||||
# Deprecated alias for backward compatibility
|
||||
return self.image_urls
|
||||
|
||||
@images_urls.setter
|
||||
def images_urls(self, value):
|
||||
self.image_urls = value
|
||||
def __str__(self) -> str:
|
||||
ret = f'**MessageAction** (source={self.source})\n'
|
||||
ret += f'CONTENT: {self.content}'
|
||||
if self.image_urls:
|
||||
for url in self.image_urls:
|
||||
if self.images_urls:
|
||||
for url in self.images_urls:
|
||||
ret += f'\nIMAGE_URL: {url}'
|
||||
return ret
|
||||
|
||||
@@ -66,10 +66,6 @@ def action_from_dict(action: dict) -> Action:
|
||||
if is_confirmed is not None:
|
||||
args['confirmation_state'] = is_confirmed
|
||||
|
||||
# images_urls has been renamed to image_urls
|
||||
if 'images_urls' in args:
|
||||
args['image_urls'] = args.pop('images_urls')
|
||||
|
||||
try:
|
||||
decoded_action = action_class(**args)
|
||||
if 'timeout' in action:
|
||||
|
||||
@@ -101,7 +101,7 @@ def event_to_memory(event: 'Event', max_message_chars: int) -> dict:
|
||||
d.pop('cause', None)
|
||||
d.pop('timestamp', None)
|
||||
d.pop('message', None)
|
||||
d.pop('image_urls', None)
|
||||
d.pop('images_urls', None)
|
||||
|
||||
# runnable actions have some extra fields used in the BE/FE, which should not be sent to the LLM
|
||||
if 'args' in d:
|
||||
|
||||
@@ -9,9 +9,9 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.runtime.utils.shutdown_listener import should_continue
|
||||
from openhands.storage import FileStore
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
|
||||
class EventStreamSubscriber(str, Enum):
|
||||
|
||||
@@ -7,7 +7,7 @@ from litellm import acompletion as litellm_acompletion
|
||||
from openhands.core.exceptions import UserCancelledError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm.llm import LLM, LLM_RETRY_EXCEPTIONS
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
from openhands.runtime.utils.shutdown_listener import should_continue
|
||||
|
||||
|
||||
class AsyncLLM(LLM):
|
||||
|
||||
@@ -14,9 +14,7 @@ class DebugMixin:
|
||||
|
||||
messages = messages if isinstance(messages, list) else [messages]
|
||||
debug_message = MESSAGE_SEPARATOR.join(
|
||||
self._format_message_content(msg)
|
||||
for msg in messages
|
||||
if msg.get('content', None)
|
||||
self._format_message_content(msg) for msg in messages if msg['content']
|
||||
)
|
||||
|
||||
if debug_message:
|
||||
|
||||