Skip to content

Commit

Permalink
Merge branch 'All-Hands-AI:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
RainRat authored Jan 9, 2025
2 parents 211f120 + 3cc20a2 commit b736fc8
Show file tree
Hide file tree
Showing 43 changed files with 781 additions and 516 deletions.
19 changes: 5 additions & 14 deletions docs/modules/usage/llms/llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,14 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
## Model Recommendations

Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and
[this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).

When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:

- Claude 3.5 Sonnet is the best by a fair amount, achieving a 53% resolve rate on SWE-Bench Verified with the default agent in OpenHands.
- GPT-4o lags behind, and o1-mini actually performed somewhat worse than GPT-4o. We went in and analyzed the results a little, and briefly it seemed like o1 was sometimes "overthinking" things, performing extra environment configuration tasks when it could just go ahead and finish the task.
- Finally, the strongest open models were Llama 3.1 405 B and deepseek-v2.5, and they performed reasonably, even besting some of the closed models.

Please refer to the [full article](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) for more details.
recommendations for model selection. Our latest benchmarking results can be found in [this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).

Based on these findings and community feedback, the following models have been verified to work reasonably well with OpenHands:

- claude-3-5-sonnet (recommended)
- gpt-4 / gpt-4o
- llama-3.1-405b
- deepseek-v2.5
- anthropic/claude-3-5-sonnet-20241022 (recommended)
- anthropic/claude-3-5-haiku-20241022
- deepseek/deepseek-chat
- gpt-4o

:::warning
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();

afterEach(() => {
vi.clearAllMocks();
Expand All @@ -18,8 +19,8 @@ describe("ConversationCard", () => {
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
Expand All @@ -38,8 +39,8 @@ describe("ConversationCard", () => {
const { rerender } = render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
Expand All @@ -53,8 +54,8 @@ describe("ConversationCard", () => {
rerender(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
Expand All @@ -64,32 +65,13 @@ describe("ConversationCard", () => {
screen.getByTestId("conversation-card-selected-repository");
});

it("should call onClick when the card is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

const card = screen.getByTestId("conversation-card");
await user.click(card);

expect(onClick).toHaveBeenCalled();
});

it("should toggle a context menu when clicking the ellipsis button", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
Expand All @@ -112,8 +94,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand All @@ -136,8 +118,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository="org/selectedRepository"
Expand All @@ -157,8 +139,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
Expand Down Expand Up @@ -189,8 +171,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand All @@ -213,8 +195,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand All @@ -232,8 +214,8 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand All @@ -252,12 +234,126 @@ describe("ConversationCard", () => {
expect(onClick).not.toHaveBeenCalled();
});

it("should call onDownloadWorkspace when the download button is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);

const menu = screen.getByTestId("context-menu");
const downloadButton = within(menu).getByTestId("download-button");

await user.click(downloadButton);

expect(onDownloadWorkspace).toHaveBeenCalled();
});

it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ConversationCard
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);

expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();

// toggle to hide the context menu
await user.click(ellipsisButton);

rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

await user.click(ellipsisButton);

expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
});

it("should not render the ellipsis button if there are no actions", () => {
const { rerender } = render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument();
});

describe("state indicator", () => {
it("should render the 'STOPPED' indicator by default", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand All @@ -271,8 +367,8 @@ describe("ConversationCard", () => {
it("should render the other indicators when provided", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@ import {
QueryClientConfig,
} from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
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";

describe("ConversationPanel", () => {
const onCloseMock = vi.fn();
const RouterStub = createRoutesStub([
{
Component: () => <ConversationPanel onClose={onCloseMock} />,
path: "/",
},
]);

const renderConversationPanel = (config?: QueryClientConfig) =>
render(<ConversationPanel onClose={onCloseMock} />, {
render(<RouterStub />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient(config)}>
Expand Down
Loading

0 comments on commit b736fc8

Please sign in to comment.