Compare commits

..

15 Commits

Author SHA1 Message Date
Graham Neubig ddd6bb3830 Update run_infer to add github prior knowledge 2025-01-21 21:25:08 -05:00
Engel Nyst f0dbb02ee1 Adjust prompt to use view command (#5506)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-21 23:50:39 +01:00
tofarr 318c811817 Added check to shutdown hook (#6402) 2025-01-21 22:32:46 +00:00
Xingyao Wang b468150f2a fix(codeact): make sure agent sees the prefix/suffix as part of observation (#6400) 2025-01-21 21:54:57 +00:00
Engel Nyst b9a3f1c753 Fix eval on remote runtime (#6398) 2025-01-21 20:49:30 +00:00
tofarr 09e8a1eeba Fix: Keeping runtimes alive again (For now) (#6395) 2025-01-21 19:20:35 +00:00
Xingyao Wang ff3880c76d fix(remote_runtime): define runtime_id first to fix attrbute error (#6393) 2025-01-21 18:13:43 +00:00
Calvin Smith 8bd7613724 fix: Settings modal properly tracks if an API key is set (#6394)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-21 11:04:30 -07:00
Engel Nyst 5b7fcfbe1a Disable prompt extensions in SWE-bench (#6391) 2025-01-21 17:18:30 +00:00
Robert Brennan 8ae36481df Fix API key again (#6390) 2025-01-21 17:00:59 +00:00
Robert Brennan 25fdb0c3bf fix api key value (#6388) 2025-01-21 16:15:28 +00:00
louria 7f57dbebda Update MiniWoB README (#6385) 2025-01-21 16:26:47 +01:00
dependabot[bot] 54589d7e83 chore(deps-dev): bump pre-commit from 4.0.1 to 4.1.0 in the pre-commit group (#6384)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 15:10:20 +00:00
Boxuan Li b7f34c3f8d (feat) Add button to export trajectory on chat panel (#6378) 2025-01-21 22:10:00 +08:00
dependabot[bot] 210eeee94a chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 13:46:56 +04:00
23 changed files with 517 additions and 1191 deletions
+8 -7
View File
@@ -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,4 +1,4 @@
import { screen, waitFor, within } from "@testing-library/react";
import { screen, 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";
describe("Sidebar", () => {
const renderSidebar = () => {
const RouterStub = createRoutesStub([
{
path: "/conversation/:conversationId",
@@ -16,9 +16,10 @@ describe("Sidebar", () => {
},
]);
const renderSidebar = () =>
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
};
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVERSATION_UI)(
"should have the conversation panel open by default",
() => {
@@ -53,16 +54,9 @@ describe("Sidebar", () => {
vi.clearAllMocks();
});
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 fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});
it("should send all settings data when saving AI configuration", async () => {
+1 -89
View File
@@ -1,111 +1,23 @@
import axios, { AxiosError } from "axios";
import { KeycloakErrorResponse } from "./open-hands.types";
import axios from "axios";
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);
},
);
};
+6 -25
View File
@@ -148,6 +148,7 @@ 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,17 +161,8 @@ class OpenHands {
static async refreshToken(
appMode: GetConfigResponse["APP_MODE"],
userId: string,
): Promise<{
keycloakAccessToken: string;
providerAccessToken: string;
keycloakUserId: string;
}> {
if (appMode === "oss")
return {
keycloakAccessToken: "",
providerAccessToken: "",
keycloakUserId: "",
};
): Promise<string> {
if (appMode === "oss") return "";
const response = await openHands.post<GitHubAccessTokenResponse>(
"/api/refresh-token",
@@ -178,11 +170,7 @@ class OpenHands {
userId,
},
);
return {
keycloakAccessToken: response.data.keycloakAccessToken,
providerAccessToken: response.data.providerAccessToken,
keycloakUserId: response.data.keycloakUserId,
};
return response.data.access_token;
}
/**
@@ -203,20 +191,13 @@ class OpenHands {
*/
static async getGitHubAccessToken(
code: string,
redirectUri: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.get<GitHubAccessTokenResponse>(
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/api/github/callback",
{
params: {
code,
redirectUri,
},
code,
},
);
console.debug(
`/api/github/callback response data: ${JSON.stringify(data)}`,
);
return data;
}
+1 -7
View File
@@ -26,9 +26,7 @@ export interface FeedbackResponse {
}
export interface GitHubAccessTokenResponse {
keycloakAccessToken: string;
providerAccessToken: string;
keycloakUserId: string;
access_token: string;
}
export interface AuthenticationResponse {
@@ -67,10 +65,6 @@ export interface AuthenticateResponse {
error?: string;
}
export interface KeycloakErrorResponse {
keycloak_error: string;
}
export interface Conversation {
conversation_id: string;
title: string;
@@ -13,6 +13,7 @@ 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";
@@ -20,7 +21,6 @@ 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,7 +28,13 @@ export function Sidebar() {
const user = useGitHubUser();
const { data: isAuthed } = useIsAuthed();
const { logout } = useAuth();
const { isUpToDate: settingsAreUpToDate, settings } = useCurrentSettings();
const {
data: settings,
isError: settingsIsError,
isSuccess: settingsSuccessfulyFetched,
} = useSettings();
const { isUpToDate: settingsAreUpToDate } = useCurrentSettings();
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
@@ -60,8 +66,6 @@ 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">
@@ -106,12 +110,13 @@ export function Sidebar() {
{accountSettingsModalOpen && (
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
)}
{showSettingsModal && (
<SettingsModal
settings={settings || DEFAULT_SETTINGS}
onClose={() => setSettingsModalIsOpen(false)}
/>
)}
{settingsIsError ||
(showSettingsModal && settingsSuccessfulyFetched && (
<SettingsModal
settings={settings}
onClose={() => setSettingsModalIsOpen(false)}
/>
))}
</>
);
}
@@ -17,10 +17,6 @@ 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);
@@ -28,7 +28,7 @@ export function AccountSettingsForm({
gitHubError,
analyticsConsent,
}: AccountSettingsFormProps) {
const { gitHubToken, keycloakToken, setAccessTokens, logout } = useAuth();
const { gitHubToken, setGitHubToken, logout } = useAuth();
const { data: config } = useConfig();
const { saveUserSettings } = useCurrentSettings();
const { t } = useTranslation();
@@ -38,11 +38,10 @@ 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 && kcToken) setAccessTokens(ghToken, kcToken);
if (ghToken) setGitHubToken(ghToken);
// The form returns the language label, so we need to find the corresponding
// language key to save it in the settings
@@ -96,12 +95,6 @@ export function AccountSettingsForm({
type="password"
defaultValue={gitHubToken ?? ""}
/>
<CustomInput
name="kcToken"
label="Keycloak Token"
type="password"
defaultValue={keycloakToken ?? ""}
/>
<BaseModalDescription>
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
<a
@@ -14,15 +14,15 @@ interface ConnectToGitHubModalProps {
}
export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
const { gitHubToken, keycloakToken, setAccessTokens } = useAuth();
const { gitHubToken, setGitHubToken } = useAuth();
const { t } = useTranslation();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const ghToken = formData.get("ghToken")?.toString();
const kcToken = formData.get("kcToken")?.toString();
if (ghToken && kcToken) setAccessTokens(ghToken, kcToken);
if (ghToken) setGitHubToken(ghToken);
onClose();
};
@@ -54,13 +54,6 @@ 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
+27 -52
View File
@@ -4,23 +4,18 @@ 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;
setAccessTokens: (
gitHubToken: string | null,
keycloakToken: string | null,
) => void;
clearAccessTokens: () => void;
setGitHubToken: (token: string | null) => void;
clearGitHubToken: () => void;
refreshToken: () => Promise<boolean>;
logout: () => void;
}
@@ -32,88 +27,69 @@ function AuthProvider({ children }: React.PropsWithChildren) {
() => localStorage.getItem("ghToken"),
);
const [keycloakTokenState, setKeycloakTokenState] = React.useState<
string | null
>(() => localStorage.getItem("kcToken"));
const [, setUserIdState] = React.useState<string>(
const [userIdState, setUserIdState] = React.useState<string>(
() => localStorage.getItem("userId") || "",
);
const clearAccessTokens = () => {
console.debug("clearAccessTokens");
const clearGitHubToken = () => {
setGitHubTokenState(null);
setKeycloakTokenState(null);
setUserIdState("");
localStorage.removeItem("ghToken");
localStorage.removeItem("kcToken");
localStorage.removeItem("userId");
removeOpenHandsGitHubTokenHeader();
removeGitHubAuthTokenHeader();
};
const setAccessTokens = (
gitHubToken: string | null,
keycloakToken: string | null,
) => {
console.debug(
`setAccessTokens keycloakToken: ${keycloakToken}, githubToken: ${gitHubToken}`,
);
setGitHubTokenState(gitHubToken);
setKeycloakTokenState(keycloakToken);
const setGitHubToken = (token: string | null) => {
setGitHubTokenState(token);
if (gitHubToken && keycloakToken) {
localStorage.setItem("ghToken", gitHubToken);
localStorage.setItem("kcToken", keycloakToken);
setOpenHandsGitHubTokenHeader(keycloakToken);
setGitHubAuthTokenHeader(gitHubToken);
if (token) {
localStorage.setItem("ghToken", token);
setOpenHandsGitHubTokenHeader(token);
setGitHubAuthTokenHeader(token);
} else {
clearAccessTokens();
clearGitHubToken();
}
};
const setUserId = (userId: string) => {
console.debug(`setUserId userId: ${userId}`);
setUserIdState(userId);
setUserIdState(userIdState);
localStorage.setItem("userId", userId);
};
const logout = () => {
clearAccessTokens();
clearGitHubToken();
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 storedUserid = localStorage.getItem("userId") || "";
const data = await OpenHands.refreshToken("saas", storedUserid);
if (data) {
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
const newToken = await OpenHands.refreshToken(config.APP_MODE, userIdState);
if (newToken) {
setGitHubToken(newToken);
return true;
}
clearAccessTokens();
clearGitHubToken();
return false;
};
React.useEffect(() => {
const storedGitHubToken = localStorage.getItem("ghToken");
const storedKeycloakToken = localStorage.getItem("kcToken");
const userId = localStorage.getItem("userId") || "";
setAccessTokens(storedGitHubToken, storedKeycloakToken);
setGitHubToken(storedGitHubToken);
setUserId(userId);
const setupIntercepter = async () => {
const config = await OpenHands.getConfig();
setupOpenhandsAxiosInterceptors(config.APP_MODE, refreshToken, logout);
// setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
};
setupIntercepter();
@@ -122,14 +98,13 @@ function AuthProvider({ children }: React.PropsWithChildren) {
const value = React.useMemo(
() => ({
gitHubToken: gitHubTokenState,
keycloakToken: keycloakTokenState,
setAccessTokens,
setGitHubToken,
setUserId,
clearAccessTokens,
clearGitHubToken,
refreshToken,
logout,
}),
[gitHubTokenState, keycloakTokenState],
[gitHubTokenState],
);
return <AuthContext value={value}>{children}</AuthContext>;
+2 -2
View File
@@ -6,7 +6,7 @@ import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
export const useGitHubUser = () => {
const { gitHubToken } = useAuth();
const { gitHubToken, setUserId } = 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,
-4
View File
@@ -4,7 +4,6 @@ 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 {
@@ -37,12 +36,9 @@ const getSettingsQueryFn = async () => {
};
export const useSettings = () => {
const { data: isAuthed } = useIsAuthed();
const query = useQuery({
queryKey: ["settings"],
queryFn: getSettingsQueryFn,
enabled: !!isAuthed,
});
React.useEffect(() => {
+3 -2
View File
@@ -46,14 +46,15 @@ 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,
@@ -7,24 +7,19 @@ import { useAuth } from "#/context/auth-context";
function OAuthGitHubCallback() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { setAccessTokens, setUserId } = useAuth();
const { setGitHubToken } = 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, redirectUrl],
queryFn: () => OpenHands.getGitHubAccessToken(code!, redirectUrl),
queryKey: ["access_token", code],
queryFn: () => OpenHands.getGitHubAccessToken(code!),
enabled: !!code,
});
console.debug(`data: ${JSON.stringify(data)}, isSuccess: ${isSuccess}`);
React.useEffect(() => {
if (isSuccess) {
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
setUserId(data.keycloakUserId);
setGitHubToken(data.access_token);
navigate("/");
}
}, [isSuccess]);
@@ -6,10 +6,6 @@
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
// 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`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
};
@@ -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
+1 -1
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)
+1 -1
View File
@@ -219,7 +219,7 @@ class SensitiveDataFilter(logging.Filter):
sensitive_patterns.extend(env_vars)
# and some special cases
sensitive_patterns.append('JWT_SECRET_KEY')
sensitive_patterns.append('JWT_SECRET')
sensitive_patterns.append('LLM_API_KEY')
sensitive_patterns.append('GITHUB_TOKEN')
sensitive_patterns.append('SANDBOX_ENV_GITHUB_TOKEN')
+1 -1
View File
@@ -9,7 +9,7 @@ app = APIRouter(prefix='/api/github')
def require_github_token(request: Request):
github_token = request.state.github_token
github_token = request.headers.get('X-GitHub-Token')
if not github_token:
raise HTTPException(
status_code=400,
+1 -1
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(file_store_path)
return S3FileStore()
elif file_store == 'google_cloud':
return GoogleCloudFileStore(file_store_path)
return InMemoryFileStore()
+23 -103
View File
@@ -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}')
Generated
+406 -831
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -69,7 +69,6 @@ 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 = "*"