Compare commits

...

29 Commits

Author SHA1 Message Date
Chuck Butkus
1786db475c Merge branch 'aws_fixes_min' into keycloak 2025-01-21 23:06:21 -05:00
Chuck Butkus
b25477179e Merge branch 'aws_fixes_min' into keycloak 2025-01-21 23:00:18 -05:00
Chuck Butkus
0e33601ffe Cleanup 2025-01-21 19:57:45 -05:00
Chuck Butkus
3a047de788 boto3 needs a full URL for the endpoint 2025-01-21 18:53:34 -05:00
Chuck Butkus
cf4b2946a8 boto3 needs a full URL for the endpoint 2025-01-21 18:50:26 -05:00
Chuck Butkus
cd8cb25426 Merge branch 'aws_fixes_min' into keycloak 2025-01-21 16:51:12 -05:00
Chuck Butkus
20e8ff8e73 Merge branch 'main' into keycloak 2025-01-21 16:49:19 -05:00
Chuck Butkus
f30d1178f6 Consolidate minio into S3FileStore 2025-01-21 16:43:01 -05:00
chuckbutkus
53228643f9 Apply suggestions from code review
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-21 00:11:31 -05:00
chuckbutkus
7d6900c281 Merge branch 'main' into aws_fixes_min 2025-01-20 22:57:17 -05:00
Chuck Butkus
0bf05937b9 Revert file_store_location change 2025-01-20 22:27:11 -05:00
Chuck Butkus
8f42db630d Merge branch 'main' into aws_fixes 2025-01-20 22:05:58 -05:00
Chuck Butkus
ae18002373 Fix lint errors 2025-01-19 23:11:38 -05:00
Chuck Butkus
0f2584ebb0 Change console logging to debug 2025-01-19 21:42:59 -05:00
Chuck Butkus
cff81640af Fix some logging 2025-01-19 21:32:22 -05:00
Chuck Butkus
b777662c2e Merge branch 'main' into keycloak 2025-01-19 18:09:56 -05:00
Chuck Butkus
928870bc6c Update 2025-01-19 15:58:33 -05:00
Chuck Butkus
dfc32287a1 Merge branch 'main' into aws_fixes 2025-01-19 00:21:22 -05:00
Chuck Butkus
d94c65623b Update 2025-01-18 17:10:04 -05:00
Chuck Butkus
4b9545cb5b More logging 2025-01-18 01:54:35 -05:00
Chuck Butkus
7d3dc563a1 Add some logging 2025-01-18 01:23:18 -05:00
Chuck Butkus
13a602f467 Merge main into branch 2025-01-17 15:11:36 -05:00
Chuck Butkus
9146010931 Changes for AWS support 2025-01-15 23:33:12 -05:00
Chuck Butkus
ab22f0ec89 Fix settings modal on default 2025-01-15 22:16:19 -05:00
Chuck Butkus
14218421c4 Fix for settings 2025-01-13 15:08:42 -05:00
Chuck Butkus
0d0d132171 Fixes 2025-01-13 00:17:11 -05:00
Chuck Butkus
c28c323b49 Merge branch 'main' into keycloak 2025-01-12 20:53:54 -05:00
Chuck Butkus
5f2860f78e Remove extra logging 2025-01-12 18:46:38 -05:00
Chuck Butkus
cce1e53434 Initial commit 2025-01-12 18:22:20 -05:00
48 changed files with 1426 additions and 550 deletions

View File

@@ -373,7 +373,7 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
**Utilisation du micro-agent**
- `use_microagents`
- `enable_prompt_extensions`
- Type : `bool`
- Valeur par défaut : `true`
- Description : Indique si l'utilisation des micro-agents est activée ou non

View File

@@ -8,6 +8,9 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve
## Test if your environment works
Follow the instructions here https://miniwob.farama.org/content/getting_started/ & https://miniwob.farama.org/content/viewing/
to set up MiniWoB server in your local environment at http://localhost:8080/miniwob/
Access with browser the above MiniWoB URLs and see if they load correctly.
## Run Evaluation

View File

@@ -71,7 +71,7 @@ def process_git_patch(patch):
return patch
def get_config(instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -132,7 +132,7 @@ def process_instance(
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
config = get_config(instance)
config = get_config(metadata, instance)
instance_id = instance.instance_id
model_patch = instance['model_patch']
test_spec: TestSpec = instance['test_spec']

View File

@@ -158,6 +158,7 @@ def get_config(
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config

View File

@@ -1,4 +1,4 @@
import { screen, within } from "@testing-library/react";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
@@ -8,7 +8,7 @@ import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import OpenHands from "#/api/open-hands";
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
const renderSidebar = () => {
describe("Sidebar", () => {
const RouterStub = createRoutesStub([
{
path: "/conversation/:conversationId",
@@ -16,10 +16,9 @@ const renderSidebar = () => {
},
]);
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
};
const renderSidebar = () =>
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVERSATION_UI)(
"should have the conversation panel open by default",
() => {
@@ -54,9 +53,16 @@ describe("Sidebar", () => {
vi.clearAllMocks();
});
it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
it("should fetch settings data on mount and if the user is authenticated", async () => {
const authenticateSpy = vi.spyOn(OpenHands, "authenticate");
const { rerender } = renderSidebar();
expect(getSettingsSpy).not.toHaveBeenCalled();
authenticateSpy.mockResolvedValueOnce(true);
rerender(<RouterStub initialEntries={["/conversation/123"]} />);
await waitFor(() => expect(getSettingsSpy).toHaveBeenCalledOnce());
});
it("should send all settings data when saving AI configuration", async () => {
@@ -156,7 +162,7 @@ describe("Sidebar", () => {
await user.click(advancedOptionsSwitch);
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
await user.type(apiKeyInput, "**********");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",

View File

@@ -52,7 +52,7 @@
"@playwright/test": "^1.49.1",
"@react-router/dev": "^7.1.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.62.16",
"@tanstack/eslint-plugin-query": "^5.64.2",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.0",
@@ -73,7 +73,7 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.2",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
@@ -5461,10 +5461,11 @@
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.62.16",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.16.tgz",
"integrity": "sha512-VhnHSQ/hc62olLzGhlLJ4BJGWynwjs3cDMsByasKJ3zjW1YZ+6raxOv0gHHISm+VEnAY42pkMowmSWrXfL4NTw==",
"version": "5.64.2",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.64.2.tgz",
"integrity": "sha512-Xq7jRYvNtGMHjQEGUZLHgEMNB59hgTlqdmKor6cdJ6CMZ/nwmBGpnlr/dcHden7W7BPCdBVN4PWMZBICWvCNQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.18.1"
},
@@ -8739,10 +8740,11 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz",
"integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.9.1"

View File

@@ -79,7 +79,7 @@
"@playwright/test": "^1.49.1",
"@react-router/dev": "^7.1.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.62.16",
"@tanstack/eslint-plugin-query": "^5.64.2",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.0",
@@ -100,7 +100,7 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.2",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",

View File

@@ -1,23 +1,111 @@
import axios from "axios";
import axios, { AxiosError } from "axios";
import { KeycloakErrorResponse } from "./open-hands.types";
export const openHands = axios.create();
export const setAuthTokenHeader = (token: string) => {
console.debug(`setAuthTokenHeader to ${token}`);
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
};
export const setGitHubTokenHeader = (token: string) => {
console.debug(`setGitHubTokenHeader to ${token}`);
openHands.defaults.headers.common["X-GitHub-Token"] = token;
};
export const removeAuthTokenHeader = () => {
console.debug("removeAuthTokenHeader");
if (openHands.defaults.headers.common.Authorization) {
delete openHands.defaults.headers.common.Authorization;
}
};
export const removeGitHubTokenHeader = () => {
console.debug("removeGitHubTokenHeader");
if (openHands.defaults.headers.common["X-GitHub-Token"]) {
delete openHands.defaults.headers.common["X-GitHub-Token"];
}
};
/**
* Checks if response has attributes to perform refresh
*/
const canRefresh = (error: unknown): boolean =>
!!(
error instanceof AxiosError &&
error.config &&
error.response &&
error.response.status &&
error.response.data.keycloak_error
);
/**
* Checks if the data is a Keycloak error response
* @param data The data to check
* @returns Boolean indicating if the data is a Keycloak error response
*/
export const isKeycloakErrorResponse = <T extends object | Array<unknown>>(
data: T | KeycloakErrorResponse | null,
): data is KeycloakErrorResponse =>
!!data && typeof data === "object" && "keycloak_error" in data;
// Axios interceptor to handle token refresh
export const setupOpenhandsAxiosInterceptors = (
appMode: string,
refreshToken: () => Promise<boolean>,
logout: () => void,
) => {
openHands.interceptors.response.use(
// Pass successful responses through
(response) => {
const parsedData = response.data;
console.debug(
`Openhands·API·call·response·to·${response.request.responseURL}\${JSON.stringify(parsedData)}`,
);
if (isKeycloakErrorResponse(parsedData)) {
const error = new AxiosError(
"Failed",
"",
response.config,
response.request,
response,
);
throw error;
}
return response;
},
// Retry request exactly once if token is expired
async (error) => {
if (!canRefresh(error)) {
return Promise.reject(new Error("Failed to refresh token"));
}
const originalRequest = error.config;
// Check if the error is due to an expired token
if (
error.response.status === 401 &&
!originalRequest._retry // Prevent infinite retry loops
) {
originalRequest._retry = true;
try {
const refreshed = await refreshToken();
if (refreshed) {
originalRequest.headers["X-GitHub-Token"] =
openHands.defaults.headers.common["X-GitHub-Token"];
return await openHands(originalRequest);
}
logout();
return await Promise.reject(new Error("Failed to refresh token"));
} catch (refreshError) {
// If token refresh fails, evict the user
logout();
return Promise.reject(refreshError);
}
}
// If the error is not due to an expired token, propagate the error
return Promise.reject(error);
},
);
};

View File

@@ -10,6 +10,7 @@ import {
AuthenticateResponse,
Conversation,
ResultSet,
GetTrajectoryResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings } from "#/services/settings";
@@ -147,7 +148,6 @@ class OpenHands {
appMode: GetConfigResponse["APP_MODE"],
): Promise<boolean> {
if (appMode === "oss") return true;
const response =
await openHands.post<AuthenticateResponse>("/api/authenticate");
return response.status === 200;
@@ -160,8 +160,17 @@ class OpenHands {
static async refreshToken(
appMode: GetConfigResponse["APP_MODE"],
userId: string,
): Promise<string> {
if (appMode === "oss") return "";
): Promise<{
keycloakAccessToken: string;
providerAccessToken: string;
keycloakUserId: string;
}> {
if (appMode === "oss")
return {
keycloakAccessToken: "",
providerAccessToken: "",
keycloakUserId: "",
};
const response = await openHands.post<GitHubAccessTokenResponse>(
"/api/refresh-token",
@@ -169,7 +178,11 @@ class OpenHands {
userId,
},
);
return response.data.access_token;
return {
keycloakAccessToken: response.data.keycloakAccessToken,
providerAccessToken: response.data.providerAccessToken,
keycloakUserId: response.data.keycloakUserId,
};
}
/**
@@ -190,13 +203,20 @@ class OpenHands {
*/
static async getGitHubAccessToken(
code: string,
redirectUri: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
const { data } = await openHands.get<GitHubAccessTokenResponse>(
"/api/github/callback",
{
code,
params: {
code,
redirectUri,
},
},
);
console.debug(
`/api/github/callback response data: ${JSON.stringify(data)}`,
);
return data;
}
@@ -354,6 +374,15 @@ class OpenHands {
return response.data.items;
}
static async getTrajectory(
conversationId: string,
): Promise<GetTrajectoryResponse> {
const { data } = await openHands.get<GetTrajectoryResponse>(
`/api/conversations/${conversationId}/trajectory`,
);
return data;
}
}
export default OpenHands;

View File

@@ -26,7 +26,9 @@ export interface FeedbackResponse {
}
export interface GitHubAccessTokenResponse {
access_token: string;
keycloakAccessToken: string;
providerAccessToken: string;
keycloakUserId: string;
}
export interface AuthenticationResponse {
@@ -55,11 +57,20 @@ export interface GetVSCodeUrlResponse {
error?: string;
}
export interface GetTrajectoryResponse {
trajectory: unknown[] | null;
error?: string;
}
export interface AuthenticateResponse {
message?: string;
error?: string;
}
export interface KeycloakErrorResponse {
keycloak_error: string;
}
export interface Conversation {
conversation_id: string;
title: string;

View File

@@ -1,8 +1,11 @@
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { FeedbackActions } from "../feedback/feedback-actions";
import { ExportActions } from "../export/export-actions";
import { createChatMessage } from "#/services/chat-service";
import { InteractiveChatBox } from "./interactive-chat-box";
import { addUserMessage } from "#/state/chat-slice";
@@ -19,6 +22,8 @@ import { ActionSuggestions } from "./action-suggestions";
import { ContinueButton } from "#/components/shared/buttons/continue-button";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { downloadTrajectory } from "#/utils/download-files";
function getEntryPoint(
hasRepository: boolean | null,
@@ -47,6 +52,8 @@ export function ChatInterface() {
const { selectedRepository, importedProjectZip } = useSelector(
(state: RootState) => state.initialQuery,
);
const params = useParams();
const { mutate: getTrajectory } = useGetTrajectory();
const handleSendMessage = async (content: string, files: File[]) => {
if (messages.length === 0) {
@@ -90,6 +97,25 @@ export function ChatInterface() {
setFeedbackPolarity(polarity);
};
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
toast.error("ConversationId unknown, cannot download trajectory");
return;
}
getTrajectory(params.conversationId, {
onSuccess: async (data) => {
await downloadTrajectory(
params.conversationId ?? "unknown",
data.trajectory,
);
},
onError: (error) => {
toast.error(error.message);
},
});
};
const isWaitingForUserInput =
curAgentState === AgentState.AWAITING_USER_INPUT ||
curAgentState === AgentState.FINISHED;
@@ -137,6 +163,9 @@ export function ChatInterface() {
onClickShareFeedbackActionButton("negative")
}
/>
<ExportActions
onExportTrajectory={() => onClickExportTrajectoryButton()}
/>
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0">
{messages.length > 2 &&

View File

@@ -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>
);
}

View File

@@ -13,7 +13,6 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
import { useCurrentSettings } from "#/context/settings-context";
import { useSettings } from "#/hooks/query/use-settings";
import { ConversationPanel } from "../conversation-panel/conversation-panel";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import { useEndSession } from "#/hooks/use-end-session";
@@ -21,6 +20,7 @@ import { setCurrentAgentState } from "#/state/agent-slice";
import { AgentState } from "#/types/agent-state";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
import { DEFAULT_SETTINGS } from "#/services/settings";
export function Sidebar() {
const dispatch = useDispatch();
@@ -28,13 +28,7 @@ export function Sidebar() {
const user = useGitHubUser();
const { data: isAuthed } = useIsAuthed();
const { logout } = useAuth();
const {
data: settings,
isError: settingsIsError,
isSuccess: settingsSuccessfulyFetched,
} = useSettings();
const { isUpToDate: settingsAreUpToDate } = useCurrentSettings();
const { isUpToDate: settingsAreUpToDate, settings } = useCurrentSettings();
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
@@ -66,6 +60,8 @@ export function Sidebar() {
const showSettingsModal =
isAuthed && (!settingsAreUpToDate || settingsModalIsOpen);
console.warn({ isAuthed, settingsAreUpToDate, settingsModalIsOpen });
return (
<>
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
@@ -110,13 +106,12 @@ export function Sidebar() {
{accountSettingsModalOpen && (
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
)}
{settingsIsError ||
(showSettingsModal && settingsSuccessfulyFetched && (
<SettingsModal
settings={settings}
onClose={() => setSettingsModalIsOpen(false)}
/>
))}
{showSettingsModal && (
<SettingsModal
settings={settings || DEFAULT_SETTINGS}
onClose={() => setSettingsModalIsOpen(false)}
/>
)}
</>
);
}

View File

@@ -17,6 +17,10 @@ interface WaitlistModalProps {
export function WaitlistModal({ ghToken, githubAuthUrl }: WaitlistModalProps) {
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
if (ghToken) {
return null; // Do not render the modal if ghToken is available
}
const handleGitHubAuth = () => {
if (githubAuthUrl) {
handleCaptureConsent(true);

View File

@@ -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>
);
}

View File

@@ -28,7 +28,7 @@ export function AccountSettingsForm({
gitHubError,
analyticsConsent,
}: AccountSettingsFormProps) {
const { gitHubToken, setGitHubToken, logout } = useAuth();
const { gitHubToken, keycloakToken, setAccessTokens, logout } = useAuth();
const { data: config } = useConfig();
const { saveUserSettings } = useCurrentSettings();
const { t } = useTranslation();
@@ -38,10 +38,11 @@ export function AccountSettingsForm({
const formData = new FormData(event.currentTarget);
const ghToken = formData.get("ghToken")?.toString();
const kcToken = formData.get("kcToken")?.toString();
const language = formData.get("language")?.toString();
const analytics = formData.get("analytics")?.toString() === "on";
if (ghToken) setGitHubToken(ghToken);
if (ghToken && kcToken) setAccessTokens(ghToken, kcToken);
// The form returns the language label, so we need to find the corresponding
// language key to save it in the settings
@@ -95,6 +96,12 @@ export function AccountSettingsForm({
type="password"
defaultValue={gitHubToken ?? ""}
/>
<CustomInput
name="kcToken"
label="Keycloak Token"
type="password"
defaultValue={keycloakToken ?? ""}
/>
<BaseModalDescription>
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
<a

View File

@@ -14,15 +14,15 @@ interface ConnectToGitHubModalProps {
}
export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
const { gitHubToken, setGitHubToken } = useAuth();
const { gitHubToken, keycloakToken, setAccessTokens } = useAuth();
const { t } = useTranslation();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const ghToken = formData.get("ghToken")?.toString();
if (ghToken) setGitHubToken(ghToken);
const kcToken = formData.get("kcToken")?.toString();
if (ghToken && kcToken) setAccessTokens(ghToken, kcToken);
onClose();
};
@@ -54,6 +54,13 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
type="password"
defaultValue={gitHubToken ?? ""}
/>
<CustomInput
label="Keycloak Token"
name="kcToken"
required
type="password"
defaultValue={keycloakToken ?? ""}
/>
<div className="flex flex-col gap-2 w-full">
<ModalButton

View File

@@ -171,7 +171,7 @@ export function SettingsForm({
<APIKeyInput
isDisabled={!!disabled}
isSet={settings.LLM_API_KEY === "SET"}
isSet={settings.LLM_API_KEY === "**********"}
/>
{showAdvancedOptions && (

View File

@@ -4,18 +4,23 @@ import OpenHands from "#/api/open-hands";
import {
removeGitHubTokenHeader as removeOpenHandsGitHubTokenHeader,
setGitHubTokenHeader as setOpenHandsGitHubTokenHeader,
setupOpenhandsAxiosInterceptors,
} from "#/api/open-hands-axios";
import {
setAuthTokenHeader as setGitHubAuthTokenHeader,
removeAuthTokenHeader as removeGitHubAuthTokenHeader,
setupAxiosInterceptors as setupGithubAxiosInterceptors,
// setupAxiosInterceptors as setupGithubAxiosInterceptors,
} from "#/api/github-axios-instance";
interface AuthContextType {
gitHubToken: string | null;
keycloakToken: string | null;
setUserId: (userId: string) => void;
setGitHubToken: (token: string | null) => void;
clearGitHubToken: () => void;
setAccessTokens: (
gitHubToken: string | null,
keycloakToken: string | null,
) => void;
clearAccessTokens: () => void;
refreshToken: () => Promise<boolean>;
logout: () => void;
}
@@ -27,69 +32,88 @@ function AuthProvider({ children }: React.PropsWithChildren) {
() => localStorage.getItem("ghToken"),
);
const [userIdState, setUserIdState] = React.useState<string>(
const [keycloakTokenState, setKeycloakTokenState] = React.useState<
string | null
>(() => localStorage.getItem("kcToken"));
const [, setUserIdState] = React.useState<string>(
() => localStorage.getItem("userId") || "",
);
const clearGitHubToken = () => {
const clearAccessTokens = () => {
console.debug("clearAccessTokens");
setGitHubTokenState(null);
setKeycloakTokenState(null);
setUserIdState("");
localStorage.removeItem("ghToken");
localStorage.removeItem("kcToken");
localStorage.removeItem("userId");
removeOpenHandsGitHubTokenHeader();
removeGitHubAuthTokenHeader();
};
const setGitHubToken = (token: string | null) => {
setGitHubTokenState(token);
const setAccessTokens = (
gitHubToken: string | null,
keycloakToken: string | null,
) => {
console.debug(
`setAccessTokens keycloakToken: ${keycloakToken}, githubToken: ${gitHubToken}`,
);
setGitHubTokenState(gitHubToken);
setKeycloakTokenState(keycloakToken);
if (token) {
localStorage.setItem("ghToken", token);
setOpenHandsGitHubTokenHeader(token);
setGitHubAuthTokenHeader(token);
if (gitHubToken && keycloakToken) {
localStorage.setItem("ghToken", gitHubToken);
localStorage.setItem("kcToken", keycloakToken);
setOpenHandsGitHubTokenHeader(keycloakToken);
setGitHubAuthTokenHeader(gitHubToken);
} else {
clearGitHubToken();
clearAccessTokens();
}
};
const setUserId = (userId: string) => {
setUserIdState(userIdState);
console.debug(`setUserId userId: ${userId}`);
setUserIdState(userId);
localStorage.setItem("userId", userId);
};
const logout = () => {
clearGitHubToken();
clearAccessTokens();
posthog.reset();
};
const refreshToken = async (): Promise<boolean> => {
const config = await OpenHands.getConfig();
// const config = await OpenHands.getConfig();
if (config.APP_MODE !== "saas" || !gitHubTokenState) {
return false;
}
// if (config.APP_MODE !== "saas" || !gitHubTokenState) {
// return false;
// }
const newToken = await OpenHands.refreshToken(config.APP_MODE, userIdState);
if (newToken) {
setGitHubToken(newToken);
const storedUserid = localStorage.getItem("userId") || "";
const data = await OpenHands.refreshToken("saas", storedUserid);
if (data) {
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
return true;
}
clearGitHubToken();
clearAccessTokens();
return false;
};
React.useEffect(() => {
const storedGitHubToken = localStorage.getItem("ghToken");
const storedKeycloakToken = localStorage.getItem("kcToken");
const userId = localStorage.getItem("userId") || "";
setGitHubToken(storedGitHubToken);
setAccessTokens(storedGitHubToken, storedKeycloakToken);
setUserId(userId);
const setupIntercepter = async () => {
const config = await OpenHands.getConfig();
setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
setupOpenhandsAxiosInterceptors(config.APP_MODE, refreshToken, logout);
// setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
};
setupIntercepter();
@@ -98,13 +122,14 @@ function AuthProvider({ children }: React.PropsWithChildren) {
const value = React.useMemo(
() => ({
gitHubToken: gitHubTokenState,
setGitHubToken,
keycloakToken: keycloakTokenState,
setAccessTokens,
setUserId,
clearGitHubToken,
clearAccessTokens,
refreshToken,
logout,
}),
[gitHubTokenState],
[gitHubTokenState, keycloakTokenState],
);
return <AuthContext value={value}>{children}</AuthContext>;

View File

@@ -34,7 +34,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
...newSettings,
};
if (updatedSettings.LLM_API_KEY === "SET") {
if (updatedSettings.LLM_API_KEY === "**********") {
delete updatedSettings.LLM_API_KEY;
}

View File

@@ -0,0 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
export const useGetTrajectory = () =>
useMutation({
mutationFn: (cid: string) => OpenHands.getTrajectory(cid),
});

View File

@@ -6,7 +6,7 @@ import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
export const useGitHubUser = () => {
const { gitHubToken, setUserId } = useAuth();
const { gitHubToken } = useAuth();
const { data: config } = useConfig();
const user = useQuery({
@@ -18,7 +18,7 @@ export const useGitHubUser = () => {
React.useEffect(() => {
if (user.data) {
setUserId(user.data.id.toString());
// setUserId(user.data.id.toString());
posthog.identify(user.data.login, {
company: user.data.company,
name: user.data.name,

View File

@@ -4,6 +4,7 @@ import posthog from "posthog-js";
import { AxiosError } from "axios";
import { DEFAULT_SETTINGS, getLocalStorageSettings } from "#/services/settings";
import OpenHands from "#/api/open-hands";
import { useIsAuthed } from "./use-is-authed";
const getSettingsQueryFn = async () => {
try {
@@ -36,9 +37,12 @@ const getSettingsQueryFn = async () => {
};
export const useSettings = () => {
const { data: isAuthed } = useIsAuthed();
const query = useQuery({
queryKey: ["settings"],
queryFn: getSettingsQueryFn,
enabled: !!isAuthed,
});
React.useEffect(() => {

View File

@@ -0,0 +1,5 @@
<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>

After

Width:  |  Height:  |  Size: 963 B

View File

@@ -46,15 +46,14 @@ export function ErrorBoundary() {
export default function MainApp() {
useMaybeMigrateSettings();
const { data: isAuthed, isFetching: isFetchingAuth } = useIsAuthed();
const { gitHubToken } = useAuth();
const { data: settings } = useSettings();
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(
!localStorage.getItem("analytics-consent"),
);
const { data: settings } = useSettings();
const config = useConfig();
const { data: isAuthed, isFetching: isFetchingAuth } = useIsAuthed();
const gitHubAuthUrl = useGitHubAuthUrl({
gitHubToken,

View File

@@ -7,19 +7,24 @@ import { useAuth } from "#/context/auth-context";
function OAuthGitHubCallback() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { setGitHubToken } = useAuth();
const { setAccessTokens, setUserId } = useAuth();
const code = searchParams.get("code");
const requesterUrl = new URL(window.location.href);
const redirectUrl = `${requesterUrl.origin}/oauth/github/callback`;
const { data, isSuccess, error } = useQuery({
queryKey: ["access_token", code],
queryFn: () => OpenHands.getGitHubAccessToken(code!),
queryKey: ["access_token", code, redirectUrl],
queryFn: () => OpenHands.getGitHubAccessToken(code!, redirectUrl),
enabled: !!code,
});
console.debug(`data: ${JSON.stringify(data)}, isSuccess: ${isSuccess}`);
React.useEffect(() => {
if (isSuccess) {
setGitHubToken(data.access_token);
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
setUserId(data.keycloakUserId);
navigate("/");
}
}, [isSuccess]);

View File

@@ -26,6 +26,18 @@ interface FileSystemDirectoryHandle {
): Promise<FileSystemFileHandle>;
}
interface SaveFilePickerOptions {
suggestedName?: string;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
excludeAcceptAllOption?: boolean;
}
interface Window {
showDirectoryPicker(): Promise<FileSystemDirectoryHandle>;
showSaveFilePicker(
options?: SaveFilePickerOptions,
): Promise<FileSystemFileHandle>;
}

View File

@@ -22,6 +22,13 @@ function isFileSystemAccessSupported(): boolean {
return "showDirectoryPicker" in window;
}
/**
* Checks if the Save File Picker API is supported
*/
function isSaveFilePickerSupported(): boolean {
return "showSaveFilePicker" in window;
}
/**
* Creates subdirectories and returns the final directory handle
*/
@@ -162,6 +169,39 @@ async function processBatch(
};
}
export async function downloadTrajectory(
conversationId: string,
data: unknown[] | null,
): Promise<void> {
try {
if (!isSaveFilePickerSupported()) {
throw new Error(
"Your browser doesn't support downloading folders. Please use Chrome, Edge, or another browser that supports the File System Access API.",
);
}
const options = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File",
accept: {
"application/json": [".json"],
},
},
],
};
const handle = await window.showSaveFilePicker(options);
const writable = await handle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
throw new Error(
`Failed to download file: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Downloads files from the workspace one by one
* @param initialPath Initial path to start downloading from. If not provided, downloads from root

View File

@@ -6,6 +6,10 @@
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
// const scope = "repo,user,workflow,offline_access";
console.debug(
`http://localhost:8080/realms/allhandsgithub/protocol/openid-connect/auth?client_id=allhandsgithub&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid+email+profile&state=some-state-value&nonce=222`,
);
// return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
return `http://localhost:8080/realms/allhandsgithub/protocol/openid-connect/auth?client_id=allhandsgithub&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid+email+profile&state=some-state-value&nonce=222`;
};

View File

@@ -277,16 +277,11 @@ class CodeActAgent(Agent):
# if it doesn't have tool call metadata, it was triggered by a user action
if obs.tool_call_metadata is None:
text = truncate_content(
f'\nObserved result of command executed by user:\n{obs.content}',
f'\nObserved result of command executed by user:\n{obs.to_agent_observation()}',
max_message_chars,
)
else:
text = truncate_content(
obs.content
+ f'\n[Python Interpreter: {obs.metadata.py_interpreter_path}]',
max_message_chars,
)
text += f'\n[Command finished with exit code {obs.exit_code}]'
text = truncate_content(obs.to_agent_observation(), max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
text = obs.content

View File

@@ -80,7 +80,7 @@ IPythonTool = ChatCompletionToolParam(
),
)
_FILE_EDIT_DESCRIPTION = """Edit a file.
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
@@ -216,7 +216,7 @@ LLMBasedFileEditTool = ChatCompletionToolParam(
),
)
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file

View File

@@ -1,6 +1,7 @@
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
<IMPORTANT>
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
* You should start exploring the file system with your view command, unless you need to explore more deeply.
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
* You MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
</IMPORTANT>

View File

@@ -39,7 +39,7 @@ class SandboxConfig(BaseModel):
remote_runtime_api_url: str = Field(default='http://localhost:8000')
local_runtime_url: str = Field(default='http://localhost')
keep_runtime_alive: bool = Field(default=False)
keep_runtime_alive: bool = Field(default=True)
rm_all_containers: bool = Field(default=False)
api_key: str | None = Field(default=None)
base_container_image: str = Field(

View 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)

View File

@@ -219,7 +219,7 @@ class SensitiveDataFilter(logging.Filter):
sensitive_patterns.extend(env_vars)
# and some special cases
sensitive_patterns.append('JWT_SECRET')
sensitive_patterns.append('JWT_SECRET_KEY')
sensitive_patterns.append('LLM_API_KEY')
sensitive_patterns.append('GITHUB_TOKEN')
sensitive_patterns.append('SANDBOX_ENV_GITHUB_TOKEN')

View File

@@ -149,16 +149,18 @@ class CmdOutputObservation(Observation):
f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code}, '
f'metadata={json.dumps(self.metadata.model_dump(), indent=2)})**\n'
'--BEGIN AGENT OBSERVATION--\n'
f'{self._to_agent_observation()}\n'
f'{self.to_agent_observation()}\n'
'--END AGENT OBSERVATION--'
)
def _to_agent_observation(self) -> str:
def to_agent_observation(self) -> str:
ret = f'{self.metadata.prefix}{self.content}{self.metadata.suffix}'
if self.metadata.working_dir:
ret += f'\n[Current working directory: {self.metadata.working_dir}]'
if self.metadata.py_interpreter_path:
ret += f'\n[Python interpreter: {self.metadata.py_interpreter_path}]'
if self.metadata.exit_code != -1:
ret += f'\n[Command finished with exit code {self.metadata.exit_code}]'
return ret

View File

@@ -66,7 +66,7 @@ class DockerRuntime(ActionExecutionClient):
headless_mode: bool = True,
):
global _atexit_registered
if not _atexit_registered:
if not _atexit_registered and not config.sandbox.keep_runtime_alive:
_atexit_registered = True
atexit.register(remove_all_runtime_containers)

View File

@@ -31,6 +31,9 @@ class RemoteRuntime(ActionExecutionClient):
"""This runtime will connect to a remote oh-runtime-client."""
port: int = 60000 # default port for the remote runtime client
runtime_id: str | None = None
runtime_url: str | None = None
_runtime_initialized: bool = False
def __init__(
self,
@@ -71,10 +74,7 @@ class RemoteRuntime(ActionExecutionClient):
self.config.sandbox.api_key,
self.session,
)
self.runtime_id: str | None = None
self.runtime_url: str | None = None
self.available_hosts: dict[str, int] = {}
self._runtime_initialized: bool = False
def log(self, level: str, message: str) -> None:
message = f'[runtime session_id={self.sid} runtime_id={self.runtime_id or "unknown"}] {message}'

View File

@@ -27,6 +27,7 @@ from openhands.server.routes.manage_conversations import (
from openhands.server.routes.public import app as public_api_router
from openhands.server.routes.security import app as security_api_router
from openhands.server.routes.settings import app as settings_router
from openhands.server.routes.trajectory import app as trajectory_router
from openhands.server.shared import openhands_config, session_manager
from openhands.utils.import_utils import get_impl
@@ -69,6 +70,7 @@ app.include_router(conversation_api_router)
app.include_router(manage_conversation_api_router)
app.include_router(settings_router)
app.include_router(github_api_router)
app.include_router(trajectory_router)
AttachConversationMiddlewareImpl = get_impl(
AttachConversationMiddleware, openhands_config.attach_conversation_middleware_path

View File

@@ -37,7 +37,11 @@ async def connect(connection_id: str, environ, auth):
if not signed_token:
logger.error('No github_auth cookie')
raise ConnectionRefusedError('No github_auth cookie')
decoded = jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256'])
if not config.jwt_secret:
raise RuntimeError('JWT secret not found')
decoded = jwt.decode(
signed_token, config.jwt_secret.get_secret_value(), algorithms=['HS256']
)
user_id = decoded['github_user_id']
logger.info(f'User {user_id} is connecting to conversation {conversation_id}')

View File

@@ -9,7 +9,7 @@ app = APIRouter(prefix='/api/github')
def require_github_token(request: Request):
github_token = request.headers.get('X-GitHub-Token')
github_token = request.state.github_token
if not github_token:
raise HTTPException(
status_code=400,

View File

@@ -0,0 +1,40 @@
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from openhands.core.logger import openhands_logger as logger
from openhands.events.serialization import event_to_trajectory
from openhands.events.stream import AsyncEventStreamWrapper
app = APIRouter(prefix='/api/conversations/{conversation_id}')
@app.get('/trajectory')
async def get_trajectory(request: Request):
"""Get trajectory.
This function retrieves the current trajectory and returns it.
Args:
request (Request): The incoming request object.
Returns:
JSONResponse: A JSON response containing the trajectory as a list of
events.
"""
try:
async_stream = AsyncEventStreamWrapper(
request.state.conversation.event_stream, filter_hidden=True
)
trajectory = []
async for event in async_stream:
trajectory.append(event_to_trajectory(event))
return JSONResponse(status_code=200, content={'trajectory': trajectory})
except Exception as e:
logger.error(f'Error getting trajectory: {e}', exc_info=True)
return JSONResponse(
status_code=500,
content={
'trajectory': None,
'error': f'Error getting trajectory: {e}',
},
)

View File

@@ -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()
return S3FileStore(file_store_path)
elif file_store == 'google_cloud':
return GoogleCloudFileStore(file_store_path)
return InMemoryFileStore()

View File

@@ -1,50 +1,130 @@
import io
import os
from minio import Minio
import boto3
import botocore
from openhands.storage.files import FileStore
class S3FileStore(FileStore):
def __init__(self) -> None:
def __init__(self, bucket_name: str | None) -> 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'
self.bucket = os.getenv('AWS_S3_BUCKET')
self.client = Minio(endpoint, access_key, secret_key, secure=secure)
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,
)
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:
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}')
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}"
)
def read(self, path: str) -> str:
try:
return self.client.get_object(self.bucket, path).data.decode('utf-8')
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}"
)
except Exception as e:
raise FileNotFoundError(f'Failed to read from S3 at path {path}: {e}')
raise FileNotFoundError(
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
)
def list(self, path: str) -> list[str]:
if path and path != '/' and not path.endswith('/'):
path += '/'
try:
return [
obj.object_name for obj in self.client.list_objects(self.bucket, path)
]
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']}")
except Exception as e:
raise FileNotFoundError(f'Failed to list S3 objects at path {path}: {e}')
raise FileNotFoundError(
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
)
def delete(self, path: str) -> None:
try:
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)
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}"
)
except Exception as e:
raise FileNotFoundError(f'Failed to delete S3 object at path {path}: {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

View File

@@ -100,7 +100,9 @@ class EmbeddingsLoader:
return AzureOpenAIEmbedding(
model='text-embedding-ada-002',
deployment_name=llm_config.embedding_deployment_name,
api_key=llm_config.api_key,
api_key=llm_config.api_key.get_secret_value()
if llm_config.api_key
else None,
azure_endpoint=llm_config.base_url,
api_version=llm_config.api_version,
)

1245
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,7 @@ openhands-aci = "0.1.8"
python-socketio = "^5.11.4"
redis = "^5.2.0"
sse-starlette = "^2.1.3"
pre-commit = "^4.0.1"
[tool.poetry.group.llama-index.dependencies]
llama-index = "*"
@@ -84,7 +85,7 @@ llama-index-embeddings-voyageai = "*"
[tool.poetry.group.dev.dependencies]
ruff = "0.9.2"
mypy = "1.14.1"
pre-commit = "4.0.1"
pre-commit = "4.1.0"
build = "*"
[tool.poetry.group.test.dependencies]

View File

@@ -46,7 +46,7 @@ def agent() -> CodeActAgent:
agent = CodeActAgent(llm=LLM(LLMConfig()), config=config)
agent.llm = Mock()
agent.llm.config = Mock()
agent.llm.config.max_message_chars = 100
agent.llm.config.max_message_chars = 1000
return agent
@@ -65,10 +65,15 @@ def test_cmd_output_observation_message(agent: CodeActAgent):
content='Command output',
metadata=CmdOutputMetadata(
exit_code=0,
prefix='[THIS IS PREFIX]',
suffix='[THIS IS SUFFIX]',
),
)
results = agent.get_observation_message(obs, tool_call_id_to_message={})
tool_call_id_to_message = {}
results = agent.get_observation_message(
obs, tool_call_id_to_message=tool_call_id_to_message
)
assert len(results) == 1
result = results[0]
@@ -76,8 +81,10 @@ def test_cmd_output_observation_message(agent: CodeActAgent):
assert result.role == 'user'
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert 'Command output' in result.content[0].text
assert 'Observed result of command executed by user:' in result.content[0].text
assert '[Command finished with exit code 0]' in result.content[0].text
assert '[THIS IS PREFIX]' in result.content[0].text
assert '[THIS IS SUFFIX]' in result.content[0].text
def test_ipython_run_cell_observation_message(agent: CodeActAgent):