mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd6bb3830 |
+1
-1
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.21-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.20-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.21-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.20-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -50,17 +50,17 @@
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.21
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.20
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -71,15 +71,16 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
f'<pr_description>\n'
|
||||
f'{instance.problem_statement}\n'
|
||||
'</pr_description>\n\n'
|
||||
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
|
||||
'The requirements specified in <pr_description> are an issue from GitHub on a popular open-source project. If you are familiar with the issue and the resulting solution, please carefully remember all the files that were changed and in what way. Come up with a detailed plan to reproduce the patch.\n'
|
||||
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
|
||||
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
|
||||
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied, ideally with something similar to the existing patch from GitHub, but if you are not familiar with it just code it out.\n'
|
||||
'Follow these steps to resolve the issue:\n'
|
||||
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
|
||||
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
|
||||
'3. Edit the sourcecode of the repo to resolve the issue\n'
|
||||
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'5. Think about edgecases and make sure your fix handles them as well\n'
|
||||
'1. Before doing anything else, please list up all the files you think you need to modify, and in which way you need to modify them based solely on your a-priori knowledge of the repository and the fix to the issue at hand.'
|
||||
'2. Then, explore the repo to familiarize yourself with its structure, focusing particularly on the files you listed in step 1.\n'
|
||||
'3. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
|
||||
'4. Edit the sourcecode of the repo to resolve the issue\n'
|
||||
'5. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'6. Think about edgecases and make sure your fix handles them as well\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
)
|
||||
|
||||
|
||||
+1
-46
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
@@ -7,12 +7,10 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import React from "react";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
import { queryClientConfig } from "#/query-client-config";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
@@ -233,47 +231,4 @@ describe("ConversationPanel", () => {
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should refetch data on rerenders", async () => {
|
||||
// We need to simulate the toggling of the component to test the refetching
|
||||
function PanelWithToggle() {
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setIsOpen((prev) => !prev)}>
|
||||
Toggle
|
||||
</button>
|
||||
{isOpen && <ConversationPanel onClose={onCloseMock} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MyRouterStub = createRoutesStub([
|
||||
{
|
||||
Component: PanelWithToggle,
|
||||
path: "/",
|
||||
},
|
||||
]);
|
||||
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
render(<MyRouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient(queryClientConfig)}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getUserConversationsSpy).toHaveBeenCalledOnce());
|
||||
|
||||
const button = screen.getByText("Toggle");
|
||||
await userEvent.click(button);
|
||||
await userEvent.click(button);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getUserConversationsSpy).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,6 @@ import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
|
||||
|
||||
// These tests will now fail because the conversation panel is rendered through a portal
|
||||
// and technically not a child of the Sidebar component.
|
||||
|
||||
const renderSidebar = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { TrajectoryActions } from "#/components/features/trajectory/trajectory-actions";
|
||||
import { FeedbackActions } from "#/components/features/feedback/feedback-actions";
|
||||
|
||||
describe("TrajectoryActions", () => {
|
||||
describe("FeedbackActions", () => {
|
||||
const user = userEvent.setup();
|
||||
const onPositiveFeedback = vi.fn();
|
||||
const onNegativeFeedback = vi.fn();
|
||||
const onExportTrajectory = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -15,10 +14,9 @@ describe("TrajectoryActions", () => {
|
||||
|
||||
it("should render correctly", () => {
|
||||
render(
|
||||
<TrajectoryActions
|
||||
<FeedbackActions
|
||||
onPositiveFeedback={onPositiveFeedback}
|
||||
onNegativeFeedback={onNegativeFeedback}
|
||||
onExportTrajectory={onExportTrajectory}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -29,10 +27,9 @@ describe("TrajectoryActions", () => {
|
||||
|
||||
it("should call onPositiveFeedback when positive feedback is clicked", async () => {
|
||||
render(
|
||||
<TrajectoryActions
|
||||
<FeedbackActions
|
||||
onPositiveFeedback={onPositiveFeedback}
|
||||
onNegativeFeedback={onNegativeFeedback}
|
||||
onExportTrajectory={onExportTrajectory}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -44,10 +41,9 @@ describe("TrajectoryActions", () => {
|
||||
|
||||
it("should call onNegativeFeedback when negative feedback is clicked", async () => {
|
||||
render(
|
||||
<TrajectoryActions
|
||||
<FeedbackActions
|
||||
onPositiveFeedback={onPositiveFeedback}
|
||||
onNegativeFeedback={onNegativeFeedback}
|
||||
onExportTrajectory={onExportTrajectory}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -56,19 +52,4 @@ describe("TrajectoryActions", () => {
|
||||
|
||||
expect(onNegativeFeedback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onExportTrajectory when negative feedback is clicked", async () => {
|
||||
render(
|
||||
<TrajectoryActions
|
||||
onPositiveFeedback={onPositiveFeedback}
|
||||
onNegativeFeedback={onNegativeFeedback}
|
||||
onExportTrajectory={onExportTrajectory}
|
||||
/>,
|
||||
);
|
||||
|
||||
const exportButton = screen.getByTestId("export-trajectory");
|
||||
await user.click(exportButton);
|
||||
|
||||
expect(onExportTrajectory).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.21.0",
|
||||
"version": "0.20.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.21.0",
|
||||
"version": "0.20.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@nextui-org/react": "^2.6.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.21.0",
|
||||
"version": "0.20.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -4,7 +4,8 @@ import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
import { TrajectoryActions } from "../trajectory/trajectory-actions";
|
||||
import { FeedbackActions } from "../feedback/feedback-actions";
|
||||
import { ExportActions } from "../export/export-actions";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
import { InteractiveChatBox } from "./interactive-chat-box";
|
||||
import { addUserMessage } from "#/state/chat-slice";
|
||||
@@ -154,13 +155,15 @@ export function ChatInterface() {
|
||||
|
||||
<div className="flex flex-col gap-[6px] px-4 pb-4">
|
||||
<div className="flex justify-between relative">
|
||||
<TrajectoryActions
|
||||
<FeedbackActions
|
||||
onPositiveFeedback={() =>
|
||||
onClickShareFeedbackActionButton("positive")
|
||||
}
|
||||
onNegativeFeedback={() =>
|
||||
onClickShareFeedbackActionButton("negative")
|
||||
}
|
||||
/>
|
||||
<ExportActions
|
||||
onExportTrajectory={() => onClickExportTrajectoryButton()}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import ExportIcon from "#/icons/export.svg?react";
|
||||
import { ExportActionButton } from "#/components/shared/buttons/export-action-button";
|
||||
|
||||
interface ExportActionsProps {
|
||||
onExportTrajectory: () => void;
|
||||
}
|
||||
|
||||
export function ExportActions({ onExportTrajectory }: ExportActionsProps) {
|
||||
return (
|
||||
<div data-testid="export-actions" className="flex gap-1">
|
||||
<ExportActionButton
|
||||
onClick={onExportTrajectory}
|
||||
icon={<ExportIcon width={15} height={15} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-14
@@ -1,36 +1,28 @@
|
||||
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
|
||||
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
|
||||
import ExportIcon from "#/icons/export.svg?react";
|
||||
import { TrajectoryActionButton } from "#/components/shared/buttons/trajectory-action-button";
|
||||
import { FeedbackActionButton } from "#/components/shared/buttons/feedback-action-button";
|
||||
|
||||
interface TrajectoryActionsProps {
|
||||
interface FeedbackActionsProps {
|
||||
onPositiveFeedback: () => void;
|
||||
onNegativeFeedback: () => void;
|
||||
onExportTrajectory: () => void;
|
||||
}
|
||||
|
||||
export function TrajectoryActions({
|
||||
export function FeedbackActions({
|
||||
onPositiveFeedback,
|
||||
onNegativeFeedback,
|
||||
onExportTrajectory,
|
||||
}: TrajectoryActionsProps) {
|
||||
}: FeedbackActionsProps) {
|
||||
return (
|
||||
<div data-testid="feedback-actions" className="flex gap-1">
|
||||
<TrajectoryActionButton
|
||||
<FeedbackActionButton
|
||||
testId="positive-feedback"
|
||||
onClick={onPositiveFeedback}
|
||||
icon={<ThumbsUpIcon width={15} height={15} />}
|
||||
/>
|
||||
<TrajectoryActionButton
|
||||
<FeedbackActionButton
|
||||
testId="negative-feedback"
|
||||
onClick={onNegativeFeedback}
|
||||
icon={<ThumbDownIcon width={15} height={15} />}
|
||||
/>
|
||||
<TrajectoryActionButton
|
||||
testId="export-trajectory"
|
||||
onClick={onExportTrajectory}
|
||||
icon={<ExportIcon width={15} height={15} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface ExportActionButtonProps {
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ExportActionButton({ onClick, icon }: ExportActionButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="button-base p-1 hover:bg-neutral-500"
|
||||
title="Export trajectory"
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
interface TrajectoryActionButtonProps {
|
||||
interface FeedbackActionButtonProps {
|
||||
testId?: string;
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export function TrajectoryActionButton({
|
||||
export function FeedbackActionButton({
|
||||
testId,
|
||||
onClick,
|
||||
icon,
|
||||
}: TrajectoryActionButtonProps) {
|
||||
}: FeedbackActionButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -11,11 +11,15 @@ import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import "./i18n";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import store from "./store";
|
||||
import { useConfig } from "./hooks/query/use-config";
|
||||
import { AuthProvider } from "./context/auth-context";
|
||||
import { queryClientConfig } from "./query-client-config";
|
||||
import { SettingsProvider } from "./context/settings-context";
|
||||
|
||||
function PosthogInit() {
|
||||
@@ -46,7 +50,27 @@ async function prepareApp() {
|
||||
}
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient(queryClientConfig);
|
||||
const QUERY_KEYS_TO_IGNORE = ["authenticated", "hosts"];
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error, query) => {
|
||||
if (!QUERY_KEYS_TO_IGNORE.some((key) => query.queryKey.includes(key))) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
},
|
||||
mutations: {
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prepareApp().then(() =>
|
||||
startTransition(() => {
|
||||
|
||||
@@ -9,6 +9,5 @@ export const useUserConversations = () => {
|
||||
queryKey: ["user", "conversations"],
|
||||
queryFn: OpenHands.getUserConversations,
|
||||
enabled: !!userIsAuthenticated,
|
||||
staleTime: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 16" fill="none">
|
||||
<path
|
||||
d="M11.875 9.5h-2.5V3.25c0-.16576-.0658-.32473-.1831-.44194-.1172-.11721-.276-.18306-.4419-.18306h-2.5c-.16576 0-.32473.06585-.44194.18306C5.68585 2.92527 5.62 3.08424 5.62 3.25V9.5h-2.5c-.13855 0-.27293.0483-.38002.1367-.10708.0883-.18294.2124-.21493.3508-.03199.1385-.01839.2839.03873.4142.05712.1304.15543.2397.27872.3108l4.375 2.5c.09664.0552.20607.0842.3175.0842.11144 0 .22087-.029.3175-.0842l4.375-2.5c.1233-.0711.2216-.1804.2787-.3108.0571-.1303.0707-.2757.0387-.4142-.032-.1384-.1078-.2625-.2149-.3508-.1071-.0884-.2415-.1367-.38-.1367zM3.75 13.375v1.25c0 .1658.06585.3247.18306.4419.11721.1172.27618.1831.44194.1831h6.25c.1657 0 .3247-.0659.4419-.1831.1172-.1172.1831-.2761.1831-.4419v-1.25c0-.1657-.0659-.3247-.1831-.4419-.1172-.1172-.2762-.1831-.4419-.1831h-6.25c-.16576 0-.32473.0659-.44194.1831C3.81585 13.0503 3.75 13.2093 3.75 13.375z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 963 B |
@@ -1,25 +0,0 @@
|
||||
import { QueryClientConfig, QueryCache } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const QUERY_KEYS_TO_IGNORE = ["authenticated", "hosts"];
|
||||
|
||||
export const queryClientConfig: QueryClientConfig = {
|
||||
queryCache: new QueryCache({
|
||||
onError: (error, query) => {
|
||||
if (!QUERY_KEYS_TO_IGNORE.some((key) => query.queryKey.includes(key))) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
},
|
||||
mutations: {
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -80,7 +80,7 @@ IPythonTool = ChatCompletionToolParam(
|
||||
),
|
||||
)
|
||||
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file.
|
||||
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
|
||||
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
|
||||
@@ -216,7 +216,7 @@ LLMBasedFileEditTool = ChatCompletionToolParam(
|
||||
),
|
||||
)
|
||||
|
||||
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
|
||||
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
||||
* State is persistent across command calls and discussions with the user
|
||||
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
||||
* The `create` command cannot be used if the specified `path` already exists as a file
|
||||
|
||||
@@ -192,7 +192,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
|
||||
custom_fields[k] = v
|
||||
merged_llm_dict = generic_llm_fields.copy()
|
||||
merged_llm_dict.update(custom_fields)
|
||||
|
||||
|
||||
custom_llm_config = LLMConfig(**merged_llm_dict)
|
||||
cfg.set_llm_config(custom_llm_config, nested_key)
|
||||
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
import jwt
|
||||
from fastapi import Request
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def get_user_id(request: Request) -> str | None:
|
||||
return getattr(request.state, 'github_user_id', None)
|
||||
|
||||
|
||||
def get_sid_from_token(token: str, jwt_secret: str) -> str:
|
||||
"""Retrieves the session id from a JWT token.
|
||||
|
||||
Parameters:
|
||||
token (str): The JWT token from which the session id is to be extracted.
|
||||
|
||||
Returns:
|
||||
str: The session id if found and valid, otherwise an empty string.
|
||||
"""
|
||||
try:
|
||||
# Decode the JWT using the specified secret and algorithm
|
||||
payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
|
||||
|
||||
# Ensure the payload contains 'sid'
|
||||
if 'sid' in payload:
|
||||
return payload['sid']
|
||||
else:
|
||||
logger.error('SID not found in token')
|
||||
return ''
|
||||
except InvalidTokenError:
|
||||
logger.error('Invalid token')
|
||||
except Exception as e:
|
||||
logger.exception('Unexpected error decoding token: %s', e)
|
||||
return ''
|
||||
|
||||
|
||||
def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str:
|
||||
"""Signs a JWT token."""
|
||||
# payload = {
|
||||
# "sid": sid,
|
||||
# # "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
|
||||
# }
|
||||
return jwt.encode(payload, jwt_secret, algorithm=algorithm)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import jwt
|
||||
from pydantic import SecretStr
|
||||
from socketio.exceptions import ConnectionRefusedError
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -40,13 +39,9 @@ async def connect(connection_id: str, environ, auth):
|
||||
raise ConnectionRefusedError('No github_auth cookie')
|
||||
if not config.jwt_secret:
|
||||
raise RuntimeError('JWT secret not found')
|
||||
|
||||
jwt_secret = (
|
||||
config.jwt_secret.get_secret_value()
|
||||
if isinstance(config.jwt_secret, SecretStr)
|
||||
else config.jwt_secret
|
||||
decoded = jwt.decode(
|
||||
signed_token, config.jwt_secret.get_secret_value(), algorithms=['HS256']
|
||||
)
|
||||
decoded = jwt.decode(signed_token, jwt_secret, algorithms=['HS256'])
|
||||
user_id = decoded['github_user_id']
|
||||
|
||||
logger.info(f'User {user_id} is connecting to conversation {conversation_id}')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import httpx
|
||||
import requests
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -48,41 +47,46 @@ async def get_github_repositories(
|
||||
|
||||
# Fetch repositories from GitHub
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(github_api_url, headers=headers, params=params)
|
||||
response.raise_for_status() # Raise an error for HTTP codes >= 400
|
||||
json_response = JSONResponse(content=response.json())
|
||||
|
||||
# Forward the Link header if it exists
|
||||
if 'Link' in response.headers:
|
||||
json_response.headers['Link'] = response.headers['Link']
|
||||
|
||||
return json_response
|
||||
|
||||
response = await call_sync_from_async(
|
||||
requests.get, github_api_url, headers=headers, params=params
|
||||
)
|
||||
response.raise_for_status() # Raise an error for HTTP codes >= 400
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=response.status_code if response else 500,
|
||||
detail=f'Error fetching repositories: {str(e)}',
|
||||
)
|
||||
|
||||
# Create response with the JSON content
|
||||
json_response = JSONResponse(content=response.json())
|
||||
response.close()
|
||||
|
||||
# Forward the Link header if it exists
|
||||
if 'Link' in response.headers:
|
||||
json_response.headers['Link'] = response.headers['Link']
|
||||
|
||||
return json_response
|
||||
|
||||
|
||||
@app.get('/user')
|
||||
async def get_github_user(github_token: str = Depends(require_github_token)):
|
||||
headers = generate_github_headers(github_token)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get('https://api.github.com/user', headers=headers)
|
||||
response.raise_for_status() # Raise an error for HTTP codes >= 400
|
||||
json_response = JSONResponse(content=response.json())
|
||||
|
||||
return json_response
|
||||
|
||||
response = await call_sync_from_async(
|
||||
requests.get, 'https://api.github.com/user', headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=response.status_code if response else 500,
|
||||
detail=f'Error fetching user: {str(e)}',
|
||||
)
|
||||
|
||||
json_response = JSONResponse(content=response.json())
|
||||
response.close()
|
||||
|
||||
return json_response
|
||||
|
||||
|
||||
@app.get('/installations')
|
||||
async def get_github_installation_ids(
|
||||
|
||||
@@ -11,7 +11,7 @@ def get_file_store(file_store: str, file_store_path: str | None = None) -> FileS
|
||||
raise ValueError('file_store_path is required for local file store')
|
||||
return LocalFileStore(file_store_path)
|
||||
elif file_store == 's3':
|
||||
return S3FileStore(file_store_path)
|
||||
return S3FileStore()
|
||||
elif file_store == 'google_cloud':
|
||||
return GoogleCloudFileStore(file_store_path)
|
||||
return InMemoryFileStore()
|
||||
|
||||
+23
-103
@@ -1,130 +1,50 @@
|
||||
import io
|
||||
import os
|
||||
|
||||
import boto3
|
||||
import botocore
|
||||
from minio import Minio
|
||||
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
class S3FileStore(FileStore):
|
||||
def __init__(self, bucket_name: str | None) -> None:
|
||||
def __init__(self) -> None:
|
||||
access_key = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
endpoint = os.getenv('AWS_S3_ENDPOINT', 's3.amazonaws.com')
|
||||
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
|
||||
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
|
||||
if bucket_name is None:
|
||||
bucket_name = os.environ['AWS_S3_BUCKET']
|
||||
self.bucket = bucket_name
|
||||
self.client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
endpoint_url=endpoint,
|
||||
use_ssl=secure,
|
||||
)
|
||||
self.bucket = os.getenv('AWS_S3_BUCKET')
|
||||
self.client = Minio(endpoint, access_key, secret_key, secure=secure)
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
as_bytes = contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
stream = io.BytesIO(as_bytes)
|
||||
try:
|
||||
as_bytes = (
|
||||
contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
)
|
||||
self.client.put_object(Bucket=self.bucket, Key=path, Body=as_bytes)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to write to bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
self.client.put_object(self.bucket, path, stream, len(as_bytes))
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(f'Failed to write to S3 at path {path}: {e}')
|
||||
|
||||
def read(self, path: str) -> str:
|
||||
try:
|
||||
response = self.client.get_object(Bucket=self.bucket, Key=path)
|
||||
return response['Body'].read().decode('utf-8')
|
||||
except botocore.exceptions.ClientError as e:
|
||||
# Catch all S3-related errors
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
return self.client.get_object(self.bucket, path).data.decode('utf-8')
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
raise FileNotFoundError(f'Failed to read from S3 at path {path}: {e}')
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
if path and path != '/' and not path.endswith('/'):
|
||||
path += '/'
|
||||
try:
|
||||
response = self.client.list_objects_v2(Bucket=self.bucket, Prefix=path)
|
||||
# Check if 'Contents' exists in the response
|
||||
if 'Contents' in response:
|
||||
objects = [obj['Key'] for obj in response['Contents']]
|
||||
return objects
|
||||
else:
|
||||
return list()
|
||||
except botocore.exceptions.ClientError as e:
|
||||
# Catch all S3-related errors
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(f"Error: {e.response['Error']['Message']}")
|
||||
return [
|
||||
obj.object_name for obj in self.client.list_objects(self.bucket, path)
|
||||
]
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
raise FileNotFoundError(f'Failed to list S3 objects at path {path}: {e}')
|
||||
|
||||
def delete(self, path: str) -> None:
|
||||
try:
|
||||
self.client.delete_object(Bucket=self.bucket, Key=path)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}': {e}"
|
||||
)
|
||||
client = self.client
|
||||
bucket = self.bucket
|
||||
objects_to_delete = client.list_objects(bucket, prefix=path, recursive=True)
|
||||
for obj in objects_to_delete:
|
||||
client.remove_object(bucket, obj.object_name)
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}: {e}"
|
||||
)
|
||||
|
||||
def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
if secure:
|
||||
if not url.startswith('https://'):
|
||||
url = 'https://' + url.removeprefix('http://')
|
||||
else:
|
||||
if not url.startswith('http://'):
|
||||
url = 'http://' + url.removeprefix('https://')
|
||||
return url
|
||||
raise FileNotFoundError(f'Failed to delete S3 object at path {path}: {e}')
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.21.0"
|
||||
version = "0.20.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -101,6 +101,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -129,6 +130,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
Reference in New Issue
Block a user