Compare commits
16 Commits
openhands-
...
add-wsl-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6e6659624 | ||
|
|
3bb8139042 | ||
|
|
9296a47ec1 | ||
|
|
1ed8628220 | ||
|
|
977108e869 | ||
|
|
02f02c975e | ||
|
|
a49d58a9fb | ||
|
|
488a320ffd | ||
|
|
377fadc2eb | ||
|
|
7df7f43e3c | ||
|
|
a45aba512a | ||
|
|
a1a9d2f175 | ||
|
|
79492b6551 | ||
|
|
80fdb9a2f4 | ||
|
|
975e75531d | ||
|
|
1b5f5bcdad |
74
.github/workflows/py-unit-tests-wsl.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Run Python Unit Tests on WSL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-on-wsl:
|
||||
name: Python Unit Tests on WSL
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup WSL
|
||||
uses: Vampire/setup-wsl@v3
|
||||
with:
|
||||
distribution: Ubuntu-22.04
|
||||
update: true
|
||||
additional-packages: python3-pip python3-venv make curl
|
||||
|
||||
- name: Set up Python in WSL
|
||||
shell: wsl-bash {0}
|
||||
run: |
|
||||
# Update package list and install software-properties-common
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y software-properties-common
|
||||
|
||||
# Install Python 3.12 from deadsnakes PPA
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa -y
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3.12 python3.12-venv python3.12-dev python3.12-distutils python3.12-pip
|
||||
|
||||
# Create symlinks for python3.12
|
||||
sudo ln -sf /usr/bin/python3.12 /usr/local/bin/python3
|
||||
sudo ln -sf /usr/bin/python3.12 /usr/local/bin/python
|
||||
sudo ln -sf /usr/bin/pip3.12 /usr/local/bin/pip3
|
||||
sudo ln -sf /usr/bin/pip3.12 /usr/local/bin/pip
|
||||
|
||||
# Install pipx and poetry
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
python3 -m pipx install poetry
|
||||
|
||||
# Add local bin to PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
echo "PATH=$PATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
shell: wsl-bash {0}
|
||||
run: poetry install --without evaluation,llama-index
|
||||
|
||||
- name: Build Environment
|
||||
shell: wsl-bash {0}
|
||||
run: make build
|
||||
|
||||
- name: Run Tests
|
||||
shell: wsl-bash {0}
|
||||
run: poetry run pytest --forked --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -44,6 +44,7 @@ docker run -it --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
|
||||
@@ -49,6 +49,7 @@ docker run -it \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
-e LLM_MODEL=$LLM_MODEL \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
|
||||
@@ -17,6 +17,7 @@ docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
|
||||
@@ -16,14 +16,14 @@ describe("Empty state", () => {
|
||||
send: vi.fn(),
|
||||
}));
|
||||
|
||||
const { useSocket: useSocketMock } = vi.hoisted(() => ({
|
||||
useSocket: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
|
||||
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
|
||||
useWsClient: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("#/context/socket", async (importActual) => ({
|
||||
...(await importActual<typeof import("#/context/socket")>()),
|
||||
useSocket: useSocketMock,
|
||||
...(await importActual<typeof import("#/context/ws-client-provider")>()),
|
||||
useWsClient: useWsClientMock,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("Empty state", () => {
|
||||
"should load the a user message to the input when selecting",
|
||||
async () => {
|
||||
// this is to test that the message is in the UI before the socket is called
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: false, // mock an inactive runtime setup
|
||||
}));
|
||||
@@ -106,7 +106,7 @@ describe("Empty state", () => {
|
||||
it.fails(
|
||||
"should send the message to the socket only if the runtime is active",
|
||||
async () => {
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: false, // mock an inactive runtime setup
|
||||
}));
|
||||
@@ -123,7 +123,7 @@ describe("Empty state", () => {
|
||||
await user.click(displayedSuggestions[0]);
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
|
||||
useSocketMock.mockImplementation(() => ({
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
runtimeActive: true, // mock an active runtime setup
|
||||
}));
|
||||
|
||||
@@ -2,8 +2,9 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach } from "node:test";
|
||||
import { useTerminal } from "#/hooks/useTerminal";
|
||||
import { SocketProvider } from "#/context/socket";
|
||||
import { Command } from "#/state/commandSlice";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
@@ -18,6 +19,17 @@ function TestTerminalComponent({
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
function Wrapper({children}: WrapperProps) {
|
||||
return (
|
||||
<WsClientProvider enabled={true} token="NO_JWT" ghToken="NO_GITHUB" settings={null}>{children}</WsClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe("useTerminal", () => {
|
||||
const mockTerminal = vi.hoisted(() => ({
|
||||
loadAddon: vi.fn(),
|
||||
@@ -50,7 +62,7 @@ describe("useTerminal", () => {
|
||||
|
||||
it("should render", () => {
|
||||
render(<TestTerminalComponent commands={[]} secrets={[]} />, {
|
||||
wrapper: SocketProvider,
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +73,7 @@ describe("useTerminal", () => {
|
||||
];
|
||||
|
||||
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
|
||||
wrapper: SocketProvider,
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
@@ -85,7 +97,7 @@ describe("useTerminal", () => {
|
||||
secrets={[secret, anotherSecret]}
|
||||
/>,
|
||||
{
|
||||
wrapper: SocketProvider,
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import PlayIcon from "#/assets/play";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
|
||||
import { RootState } from "#/store";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
const IgnoreTaskStateMap: Record<string, AgentState[]> = {
|
||||
[AgentState.PAUSED]: [
|
||||
@@ -72,7 +72,7 @@ function ActionButton({
|
||||
}
|
||||
|
||||
function AgentControlBar() {
|
||||
const { send } = useSocket();
|
||||
const { send } = useWsClient();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const handleAction = (action: AgentState) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Clip from "#/assets/clip.svg?react";
|
||||
import Clip from "#/icons/clip.svg?react";
|
||||
|
||||
export function AttachImageLabel() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import ArrowSendIcon from "#/assets/arrow-send.svg?react";
|
||||
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ChatInputProps {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { FeedbackActions } from "./feedback-actions";
|
||||
@@ -21,14 +20,15 @@ import { ContinueButton } from "./continue-button";
|
||||
import { ScrollToBottomButton } from "./scroll-to-bottom-button";
|
||||
import { Suggestions } from "./suggestions";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import BuildIt from "#/assets/build-it.svg?react";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
const isErrorMessage = (
|
||||
message: Message | ErrorMessage,
|
||||
): message is ErrorMessage => "error" in message;
|
||||
|
||||
export function ChatInterface() {
|
||||
const { send } = useSocket();
|
||||
const { send } = useWsClient();
|
||||
const dispatch = useDispatch();
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
||||
|
||||
@@ -5,7 +5,7 @@ import RejectIcon from "#/assets/reject";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
interface ActionTooltipProps {
|
||||
type: "confirm" | "reject";
|
||||
@@ -37,7 +37,7 @@ function ActionTooltip({ type, onClick }: ActionTooltipProps) {
|
||||
|
||||
function ConfirmationButtons() {
|
||||
const { t } = useTranslation();
|
||||
const { send } = useSocket();
|
||||
const { send } = useWsClient();
|
||||
|
||||
const handleStateChange = (state: AgentState) => {
|
||||
const event = generateAgentStateChangeEvent(state);
|
||||
|
||||
188
frontend/src/components/event-handler.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React from "react";
|
||||
import {
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { addErrorMessage, addUserMessage } from "#/state/chatSlice";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import {
|
||||
getCloneRepoCommand,
|
||||
getGitHubTokenCommand,
|
||||
} from "#/services/terminalService";
|
||||
import {
|
||||
clearFiles,
|
||||
clearSelectedRepository,
|
||||
setImportedProjectZip,
|
||||
} from "#/state/initial-query-slice";
|
||||
import { clientLoader as appClientLoader } from "#/routes/_oh.app";
|
||||
import store, { RootState } from "#/store";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import { clientLoader as rootClientLoader } from "#/routes/_oh";
|
||||
import { isGitHubErrorReponse } from "#/api/github";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { setCurrentAgentState } from "#/state/agentSlice";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { getSettings } from "#/services/settings";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
message: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const isServerError = (data: object): data is ServerError => "error" in data;
|
||||
|
||||
const isErrorObservation = (data: object): data is ErrorObservation =>
|
||||
"observation" in data && data.observation === "error";
|
||||
|
||||
export function EventHandler({ children }: React.PropsWithChildren) {
|
||||
const { events, status, send } = useWsClient();
|
||||
const statusRef = React.useRef<WsClientProviderStatus | null>(null);
|
||||
const runtimeActive = status === WsClientProviderStatus.ACTIVE;
|
||||
const fetcher = useFetcher();
|
||||
const dispatch = useDispatch();
|
||||
const { files, importedProjectZip } = useSelector(
|
||||
(state: RootState) => state.initalQuery,
|
||||
);
|
||||
const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
|
||||
const initialQueryRef = React.useRef<string | null>(
|
||||
store.getState().initalQuery.initialQuery,
|
||||
);
|
||||
|
||||
const sendInitialQuery = (query: string, base64Files: string[]) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
send(createChatMessage(query, base64Files, timestamp));
|
||||
};
|
||||
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
|
||||
const userId = React.useMemo(() => {
|
||||
if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
|
||||
return null;
|
||||
}, [data?.user]);
|
||||
const userSettings = getSettings();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
const event = events[events.length - 1];
|
||||
if (event.token) {
|
||||
fetcher.submit({ token: event.token as string }, { method: "post" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
fetcher.submit({}, { method: "POST", action: "/end-session" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.error === "string") {
|
||||
toast.error(event.error);
|
||||
} else {
|
||||
toast.error(event.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isErrorObservation(event)) {
|
||||
dispatch(
|
||||
addErrorMessage({
|
||||
id: event.extras?.error_id,
|
||||
message: event.message,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
handleAssistantMessage(event);
|
||||
}, [events.length]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (statusRef.current === status) {
|
||||
return; // This is a check because of strict mode - if the status did not change, don't do anything
|
||||
}
|
||||
statusRef.current = status;
|
||||
const initialQuery = initialQueryRef.current;
|
||||
|
||||
if (status === WsClientProviderStatus.ACTIVE) {
|
||||
let additionalInfo = "";
|
||||
if (ghToken && repo) {
|
||||
send(getCloneRepoCommand(ghToken, repo));
|
||||
additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
|
||||
dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
|
||||
}
|
||||
// if there's an uploaded project zip, add it to the chat
|
||||
else if (importedProjectZip) {
|
||||
additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
|
||||
}
|
||||
|
||||
if (initialQuery) {
|
||||
if (additionalInfo) {
|
||||
sendInitialQuery(`${initialQuery}\n\n[${additionalInfo}]`, files);
|
||||
} else {
|
||||
sendInitialQuery(initialQuery, files);
|
||||
}
|
||||
dispatch(clearFiles()); // reset selected files
|
||||
initialQueryRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (status === WsClientProviderStatus.OPENING && initialQuery) {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: initialQuery,
|
||||
imageUrls: files,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (status === WsClientProviderStatus.STOPPED) {
|
||||
store.dispatch(setCurrentAgentState(AgentState.STOPPED));
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (runtimeActive && userId && ghToken) {
|
||||
// Export if the user valid, this could happen mid-session so it is handled here
|
||||
send(getGitHubTokenCommand(ghToken));
|
||||
}
|
||||
}, [userId, ghToken, runtimeActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (runtimeActive && importedProjectZip) {
|
||||
// upload files action
|
||||
try {
|
||||
const blob = base64ToBlob(importedProjectZip);
|
||||
const file = new File([blob], "imported-project.zip", {
|
||||
type: blob.type,
|
||||
});
|
||||
await OpenHands.uploadFiles([file]);
|
||||
dispatch(setImportedProjectZip(null));
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload project files.");
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [runtimeActive, importedProjectZip]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (userSettings.LLM_API_KEY) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [userSettings.LLM_API_KEY]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import CloseIcon from "#/assets/close.svg?react";
|
||||
import CloseIcon from "#/icons/close.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ImagePreviewProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LoadingSpinnerOuter from "#/assets/loading-outer.svg?react";
|
||||
import LoadingSpinnerOuter from "#/icons/loading-outer.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import ModalBody from "./ModalBody";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@@ -2,17 +2,17 @@ import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import posthog from "posthog-js";
|
||||
import EllipsisH from "#/assets/ellipsis-h.svg?react";
|
||||
import EllipsisH from "#/icons/ellipsis-h.svg?react";
|
||||
import { ModalBackdrop } from "../modals/modal-backdrop";
|
||||
import { ConnectToGitHubModal } from "../modals/connect-to-github-modal";
|
||||
import { addUserMessage } from "#/state/chatSlice";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
|
||||
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
|
||||
import { ProjectMenuDetails } from "./project-menu-details";
|
||||
import { downloadWorkspace } from "#/utils/download-workspace";
|
||||
import { LoadingSpinner } from "../modals/LoadingProject";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
interface ProjectMenuCardProps {
|
||||
isConnectedToGitHub: boolean;
|
||||
@@ -27,7 +27,7 @@ export function ProjectMenuCard({
|
||||
isConnectedToGitHub,
|
||||
githubData,
|
||||
}: ProjectMenuCardProps) {
|
||||
const { send } = useSocket();
|
||||
const { send } = useWsClient();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "#/utils/utils";
|
||||
import CloudConnection from "#/assets/cloud-connection.svg?react";
|
||||
import CloudConnection from "#/icons/cloud-connection.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ProjectMenuDetailsPlaceholderProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExternalLinkIcon from "#/assets/external-link.svg?react";
|
||||
import ExternalLinkIcon from "#/icons/external-link.svg?react";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ArrowSendIcon from "#/assets/arrow-send.svg?react";
|
||||
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
|
||||
|
||||
interface ScrollToBottomButtonProps {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Lightbulb from "#/assets/lightbulb.svg?react";
|
||||
import Refresh from "#/assets/refresh.svg?react";
|
||||
import Lightbulb from "#/icons/lightbulb.svg?react";
|
||||
import Refresh from "#/icons/refresh.svg?react";
|
||||
|
||||
interface SuggestionBubbleProps {
|
||||
suggestion: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Clip from "#/assets/clip.svg?react";
|
||||
import Clip from "#/icons/clip.svg?react";
|
||||
|
||||
interface UploadImageInputProps {
|
||||
onUpload: (files: File[]) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoadingSpinner } from "./modals/LoadingProject";
|
||||
import DefaultUserAvatar from "#/assets/default-user.svg?react";
|
||||
import DefaultUserAvatar from "#/icons/default-user.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface UserAvatarProps {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import React from "react";
|
||||
import { Data } from "ws";
|
||||
import posthog from "posthog-js";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
|
||||
interface WebSocketClientOptions {
|
||||
token: string | null;
|
||||
onOpen?: (event: Event) => void;
|
||||
onMessage?: (event: MessageEvent<Data>) => void;
|
||||
onError?: (event: Event) => void;
|
||||
onClose?: (event: Event) => void;
|
||||
}
|
||||
|
||||
interface WebSocketContextType {
|
||||
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
|
||||
start: (options?: WebSocketClientOptions) => void;
|
||||
stop: () => void;
|
||||
setRuntimeIsInitialized: () => void;
|
||||
runtimeActive: boolean;
|
||||
isConnected: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
const SocketContext = React.createContext<WebSocketContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface SocketProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SocketProvider({ children }: SocketProviderProps) {
|
||||
const wsRef = React.useRef<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = React.useState(false);
|
||||
const [runtimeActive, setRuntimeActive] = React.useState(false);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const setRuntimeIsInitialized = () => {
|
||||
setRuntimeActive(true);
|
||||
};
|
||||
|
||||
const start = React.useCallback((options?: WebSocketClientOptions): void => {
|
||||
if (wsRef.current) {
|
||||
EventLogger.warning(
|
||||
"WebSocket connection is already established, but a new one is starting anyways.",
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const sessionToken = options?.token || "NO_JWT"; // not allowed to be empty or duplicated
|
||||
const ghToken = localStorage.getItem("ghToken") || "NO_GITHUB";
|
||||
|
||||
const ws = new WebSocket(`${protocol}//${baseUrl}/ws`, [
|
||||
"openhands",
|
||||
sessionToken,
|
||||
ghToken,
|
||||
]);
|
||||
|
||||
ws.addEventListener("open", (event) => {
|
||||
posthog.capture("socket_opened");
|
||||
setIsConnected(true);
|
||||
options?.onOpen?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
EventLogger.message(event);
|
||||
|
||||
setEvents((prevEvents) => [...prevEvents, JSON.parse(event.data)]);
|
||||
options?.onMessage?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", (event) => {
|
||||
posthog.capture("socket_error");
|
||||
EventLogger.event(event, "SOCKET ERROR");
|
||||
options?.onError?.(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
posthog.capture("socket_closed");
|
||||
EventLogger.event(event, "SOCKET CLOSE");
|
||||
|
||||
setIsConnected(false);
|
||||
setRuntimeActive(false);
|
||||
wsRef.current = null;
|
||||
options?.onClose?.(event);
|
||||
});
|
||||
|
||||
wsRef.current = ws;
|
||||
}, []);
|
||||
|
||||
const stop = React.useCallback((): void => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const send = React.useCallback(
|
||||
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
|
||||
if (!wsRef.current) {
|
||||
EventLogger.error("WebSocket is not connected.");
|
||||
return;
|
||||
}
|
||||
setEvents((prevEvents) => [...prevEvents, JSON.parse(data.toString())]);
|
||||
wsRef.current.send(data);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
send,
|
||||
start,
|
||||
stop,
|
||||
setRuntimeIsInitialized,
|
||||
runtimeActive,
|
||||
isConnected,
|
||||
events,
|
||||
}),
|
||||
[
|
||||
send,
|
||||
start,
|
||||
stop,
|
||||
setRuntimeIsInitialized,
|
||||
runtimeActive,
|
||||
isConnected,
|
||||
events,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useSocket() {
|
||||
const context = React.useContext(SocketContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { SocketProvider, useSocket };
|
||||
175
frontend/src/context/ws-client-provider.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import { Settings } from "#/services/settings";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
import AgentState from "#/types/AgentState";
|
||||
|
||||
export enum WsClientProviderStatus {
|
||||
STOPPED,
|
||||
OPENING,
|
||||
ACTIVE,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
interface UseWsClient {
|
||||
status: WsClientProviderStatus;
|
||||
events: Record<string, unknown>[];
|
||||
send: (event: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
status: WsClientProviderStatus.STOPPED,
|
||||
events: [],
|
||||
send: () => {
|
||||
throw new Error("not connected");
|
||||
},
|
||||
});
|
||||
|
||||
interface WsClientProviderProps {
|
||||
enabled: boolean;
|
||||
token: string | null;
|
||||
ghToken: string | null;
|
||||
settings: Settings | null;
|
||||
}
|
||||
|
||||
export function WsClientProvider({
|
||||
enabled,
|
||||
token,
|
||||
ghToken,
|
||||
settings,
|
||||
children,
|
||||
}: React.PropsWithChildren<WsClientProviderProps>) {
|
||||
const wsRef = React.useRef<WebSocket | null>(null);
|
||||
const tokenRef = React.useRef<string | null>(token);
|
||||
const ghTokenRef = React.useRef<string | null>(ghToken);
|
||||
const closeRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [status, setStatus] = React.useState(WsClientProviderStatus.STOPPED);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
|
||||
function send(event: Record<string, unknown>) {
|
||||
if (!wsRef.current) {
|
||||
EventLogger.error("WebSocket is not connected.");
|
||||
return;
|
||||
}
|
||||
wsRef.current.send(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
setStatus(WsClientProviderStatus.OPENING);
|
||||
const initEvent = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
send(initEvent);
|
||||
}
|
||||
|
||||
function handleMessage(messageEvent: MessageEvent) {
|
||||
const event = JSON.parse(messageEvent.data);
|
||||
setEvents((prevEvents) => [...prevEvents, event]);
|
||||
if (event.extras?.agent_state === AgentState.INIT) {
|
||||
setStatus(WsClientProviderStatus.ACTIVE);
|
||||
}
|
||||
if (
|
||||
status !== WsClientProviderStatus.ACTIVE &&
|
||||
event?.observation === "error"
|
||||
) {
|
||||
setStatus(WsClientProviderStatus.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setStatus(WsClientProviderStatus.STOPPED);
|
||||
setEvents([]);
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
function handleError(event: Event) {
|
||||
posthog.capture("socket_error");
|
||||
EventLogger.event(event, "SOCKET ERROR");
|
||||
setStatus(WsClientProviderStatus.ERROR);
|
||||
}
|
||||
|
||||
// Connect websocket
|
||||
React.useEffect(() => {
|
||||
let ws = wsRef.current;
|
||||
|
||||
// If disabled close any existing websockets...
|
||||
if (!enabled) {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
wsRef.current = null;
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// If there is no websocket or the tokens have changed or the current websocket is closed,
|
||||
// create a new one
|
||||
if (
|
||||
!ws ||
|
||||
(tokenRef.current && token !== tokenRef.current) ||
|
||||
ghToken !== ghTokenRef.current ||
|
||||
ws.readyState === WebSocket.CLOSED ||
|
||||
ws.readyState === WebSocket.CLOSING
|
||||
) {
|
||||
ws?.close();
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
ws = new WebSocket(`${protocol}//${baseUrl}/ws`, [
|
||||
"openhands",
|
||||
token || "NO_JWT",
|
||||
ghToken || "NO_GITHUB",
|
||||
]);
|
||||
}
|
||||
ws.addEventListener("open", handleOpen);
|
||||
ws.addEventListener("message", handleMessage);
|
||||
ws.addEventListener("error", handleError);
|
||||
ws.addEventListener("close", handleClose);
|
||||
wsRef.current = ws;
|
||||
tokenRef.current = token;
|
||||
ghTokenRef.current = ghToken;
|
||||
|
||||
return () => {
|
||||
ws.removeEventListener("open", handleOpen);
|
||||
ws.removeEventListener("message", handleMessage);
|
||||
ws.removeEventListener("error", handleError);
|
||||
ws.removeEventListener("close", handleClose);
|
||||
};
|
||||
}, [enabled, token, ghToken]);
|
||||
|
||||
// Strict mode mounts and unmounts each component twice, so we have to wait in the destructor
|
||||
// before actually closing the socket and cancel the operation if the component gets remounted.
|
||||
React.useEffect(() => {
|
||||
const timeout = closeRef.current;
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
return () => {
|
||||
closeRef.current = setTimeout(() => {
|
||||
wsRef.current?.close();
|
||||
}, 100);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo<UseWsClient>(
|
||||
() => ({
|
||||
status,
|
||||
events,
|
||||
send,
|
||||
}),
|
||||
[status, events],
|
||||
);
|
||||
|
||||
return (
|
||||
<WsClientContext.Provider value={value}>
|
||||
{children}
|
||||
</WsClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWsClient() {
|
||||
const context = React.useContext(WsClientContext);
|
||||
return context;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import React, { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { SocketProvider } from "./context/socket";
|
||||
import "./i18n";
|
||||
import store from "./store";
|
||||
|
||||
@@ -43,12 +42,10 @@ prepareApp().then(() =>
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<SocketProvider>
|
||||
<Provider store={store}>
|
||||
<RemixBrowser />
|
||||
<PosthogInit />
|
||||
</Provider>
|
||||
</SocketProvider>
|
||||
<Provider store={store}>
|
||||
<RemixBrowser />
|
||||
<PosthogInit />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import { Command } from "#/state/commandSlice";
|
||||
import { getTerminalCommand } from "#/services/terminalService";
|
||||
import { parseTerminalOutput } from "#/utils/parseTerminalOutput";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
/*
|
||||
NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component.
|
||||
@@ -15,7 +15,7 @@ export const useTerminal = (
|
||||
commands: Command[] = [],
|
||||
secrets: string[] = [],
|
||||
) => {
|
||||
const { send } = useSocket();
|
||||
const { send } = useWsClient();
|
||||
const terminal = React.useRef<Terminal | null>(null);
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 662 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 552 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 924 B After Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 856 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
@@ -1,4 +1,4 @@
|
||||
import BuildIt from "#/assets/build-it.svg?react";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
|
||||
export function HeroHeading() {
|
||||
return (
|
||||
|
||||
@@ -2,71 +2,29 @@ import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import {
|
||||
Outlet,
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
json,
|
||||
ClientActionFunctionArgs,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import WebSocket from "ws";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getSettings } from "#/services/settings";
|
||||
import Security from "../components/modals/security/Security";
|
||||
import { Controls } from "#/components/controls";
|
||||
import store, { RootState } from "#/store";
|
||||
import store from "#/store";
|
||||
import { Container } from "#/components/container";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import {
|
||||
addErrorMessage,
|
||||
addUserMessage,
|
||||
clearMessages,
|
||||
} from "#/state/chatSlice";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import {
|
||||
getGitHubTokenCommand,
|
||||
getCloneRepoCommand,
|
||||
} from "#/services/terminalService";
|
||||
import { clearMessages } from "#/state/chatSlice";
|
||||
import { clearTerminal } from "#/state/commandSlice";
|
||||
import { useEffectOnce } from "#/utils/use-effect-once";
|
||||
import CodeIcon from "#/assets/code.svg?react";
|
||||
import GlobeIcon from "#/assets/globe.svg?react";
|
||||
import ListIcon from "#/assets/list-type-number.svg?react";
|
||||
import { createChatMessage } from "#/services/chatService";
|
||||
import {
|
||||
clearFiles,
|
||||
clearInitialQuery,
|
||||
clearSelectedRepository,
|
||||
setImportedProjectZip,
|
||||
} from "#/state/initial-query-slice";
|
||||
import CodeIcon from "#/icons/code.svg?react";
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import ListIcon from "#/icons/list-type-number.svg?react";
|
||||
import { clearInitialQuery } from "#/state/initial-query-slice";
|
||||
import { isGitHubErrorReponse, retrieveLatestGitHubCommit } from "#/api/github";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import AgentState from "#/types/AgentState";
|
||||
import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { clientLoader as rootClientLoader } from "#/routes/_oh";
|
||||
import { clearJupyter } from "#/state/jupyterSlice";
|
||||
import { FilesProvider } from "#/context/files";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { ChatInterface } from "#/components/chat-interface";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
message: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const isServerError = (data: object): data is ServerError => "error" in data;
|
||||
|
||||
const isErrorObservation = (data: object): data is ErrorObservation =>
|
||||
"observation" in data && data.observation === "error";
|
||||
|
||||
const isAgentStateChange = (
|
||||
data: object,
|
||||
): data is { extras: { agent_state: AgentState } } =>
|
||||
"extras" in data &&
|
||||
data.extras instanceof Object &&
|
||||
"agent_state" in data.extras;
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { EventHandler } from "#/components/event-handler";
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const ghToken = localStorage.getItem("ghToken");
|
||||
@@ -116,174 +74,26 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
|
||||
|
||||
function App() {
|
||||
const dispatch = useDispatch();
|
||||
const { files, importedProjectZip } = useSelector(
|
||||
(state: RootState) => state.initalQuery,
|
||||
);
|
||||
const { start, send, setRuntimeIsInitialized, runtimeActive } = useSocket();
|
||||
const { settings, token, ghToken, repo, q, lastCommit } =
|
||||
const { settings, token, ghToken, lastCommit } =
|
||||
useLoaderData<typeof clientLoader>();
|
||||
const fetcher = useFetcher();
|
||||
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
|
||||
|
||||
const secrets = React.useMemo(
|
||||
() => [ghToken, token].filter((secret) => secret !== null),
|
||||
[ghToken, token],
|
||||
);
|
||||
|
||||
// To avoid re-rendering the component when the user object changes, we memoize the user ID.
|
||||
// We use this to ensure the github token is valid before exporting it to the terminal.
|
||||
const userId = React.useMemo(() => {
|
||||
if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
|
||||
return null;
|
||||
}, [data?.user]);
|
||||
|
||||
const Terminal = React.useMemo(
|
||||
() => React.lazy(() => import("../components/terminal/Terminal")),
|
||||
[],
|
||||
);
|
||||
|
||||
const addIntialQueryToChat = (
|
||||
query: string,
|
||||
base64Files: string[],
|
||||
timestamp = new Date().toISOString(),
|
||||
) => {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: query,
|
||||
imageUrls: base64Files,
|
||||
timestamp,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const sendInitialQuery = (query: string, base64Files: string[]) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
send(createChatMessage(query, base64Files, timestamp));
|
||||
};
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
const initEvent = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
send(JSON.stringify(initEvent));
|
||||
|
||||
// display query in UI, but don't send it to the server
|
||||
if (q) addIntialQueryToChat(q, files);
|
||||
}, [settings]);
|
||||
|
||||
const handleMessage = React.useCallback(
|
||||
(message: MessageEvent<WebSocket.Data>) => {
|
||||
// set token received from the server
|
||||
const parsed = JSON.parse(message.data.toString());
|
||||
if ("token" in parsed) {
|
||||
fetcher.submit({ token: parsed.token }, { method: "post" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServerError(parsed)) {
|
||||
if (parsed.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
fetcher.submit({}, { method: "POST", action: "/end-session" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof parsed.error === "string") {
|
||||
toast.error(parsed.error);
|
||||
} else {
|
||||
toast.error(parsed.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (isErrorObservation(parsed)) {
|
||||
dispatch(
|
||||
addErrorMessage({
|
||||
id: parsed.extras?.error_id,
|
||||
message: parsed.message,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
handleAssistantMessage(message.data.toString());
|
||||
|
||||
// handle first time connection
|
||||
if (
|
||||
isAgentStateChange(parsed) &&
|
||||
parsed.extras.agent_state === AgentState.INIT
|
||||
) {
|
||||
setRuntimeIsInitialized();
|
||||
|
||||
// handle new session
|
||||
if (!token) {
|
||||
let additionalInfo = "";
|
||||
if (ghToken && repo) {
|
||||
send(getCloneRepoCommand(ghToken, repo));
|
||||
additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
|
||||
dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
|
||||
}
|
||||
// if there's an uploaded project zip, add it to the chat
|
||||
else if (importedProjectZip) {
|
||||
additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
|
||||
}
|
||||
|
||||
if (q) {
|
||||
if (additionalInfo) {
|
||||
sendInitialQuery(`${q}\n\n[${additionalInfo}]`, files);
|
||||
} else {
|
||||
sendInitialQuery(q, files);
|
||||
}
|
||||
dispatch(clearFiles()); // reset selected files
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[token, ghToken, repo, q, files],
|
||||
);
|
||||
|
||||
const startSocketConnection = React.useCallback(() => {
|
||||
start({
|
||||
token,
|
||||
onOpen: handleOpen,
|
||||
onMessage: handleMessage,
|
||||
});
|
||||
}, [token, handleOpen, handleMessage]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
// clear and restart the socket connection
|
||||
dispatch(clearMessages());
|
||||
dispatch(clearTerminal());
|
||||
dispatch(clearJupyter());
|
||||
dispatch(clearInitialQuery()); // Clear initial query when navigating to /app
|
||||
startSocketConnection();
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (runtimeActive && userId && ghToken) {
|
||||
// Export if the user valid, this could happen mid-session so it is handled here
|
||||
send(getGitHubTokenCommand(ghToken));
|
||||
}
|
||||
}, [userId, ghToken, runtimeActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (runtimeActive && importedProjectZip) {
|
||||
// upload files action
|
||||
try {
|
||||
const blob = base64ToBlob(importedProjectZip);
|
||||
const file = new File([blob], "imported-project.zip", {
|
||||
type: blob.type,
|
||||
});
|
||||
await OpenHands.uploadFiles([file]);
|
||||
dispatch(setImportedProjectZip(null));
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload project files.");
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [runtimeActive, importedProjectZip]);
|
||||
|
||||
const {
|
||||
isOpen: securityModalIsOpen,
|
||||
onOpen: onSecurityModalOpen,
|
||||
@@ -291,53 +101,62 @@ function App() {
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto gap-3">
|
||||
<Container className="w-[390px] max-h-full relative">
|
||||
<ChatInterface />
|
||||
</Container>
|
||||
<WsClientProvider
|
||||
enabled
|
||||
token={token}
|
||||
ghToken={ghToken}
|
||||
settings={settings}
|
||||
>
|
||||
<EventHandler>
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
<div className="flex h-full overflow-auto gap-3">
|
||||
<Container className="w-[390px] max-h-full relative">
|
||||
<ChatInterface />
|
||||
</Container>
|
||||
|
||||
<div className="flex flex-col grow gap-3">
|
||||
<Container
|
||||
className="h-2/3"
|
||||
labels={[
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: "Browser",
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
isBeta: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<Container className="h-1/3 overflow-scroll" label="Terminal">
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
<div className="flex flex-col grow gap-3">
|
||||
<Container
|
||||
className="h-2/3"
|
||||
labels={[
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: "Browser",
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
isBeta: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<Container className="h-1/3 overflow-scroll" label="Terminal">
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[60px]">
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings.SECURITY_ANALYZER}
|
||||
lastCommitData={lastCommit}
|
||||
/>
|
||||
</div>
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[60px]">
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings.SECURITY_ANALYZER}
|
||||
lastCommitData={lastCommit}
|
||||
/>
|
||||
</div>
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
</div>
|
||||
</EventHandler>
|
||||
</WsClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,11 @@ import { DangerModal } from "#/components/modals/confirmation-modals/danger-moda
|
||||
import { LoadingSpinner } from "#/components/modals/LoadingProject";
|
||||
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
|
||||
import { UserActions } from "#/components/user-actions";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import i18n from "#/i18n";
|
||||
import { getSettings, settingsAreUpToDate } from "#/services/settings";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import NewProjectIcon from "#/assets/new-project.svg?react";
|
||||
import DocsIcon from "#/assets/docs.svg?react";
|
||||
import NewProjectIcon from "#/icons/new-project.svg?react";
|
||||
import DocsIcon from "#/icons/docs.svg?react";
|
||||
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
|
||||
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
|
||||
import { WaitlistModal } from "#/components/waitlist-modal";
|
||||
@@ -135,7 +134,6 @@ type SettingsFormData = {
|
||||
};
|
||||
|
||||
export default function MainApp() {
|
||||
const { stop, isConnected } = useSocket();
|
||||
const navigation = useNavigation();
|
||||
const location = useLocation();
|
||||
const {
|
||||
@@ -202,14 +200,6 @@ export default function MainApp() {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (location.pathname === "/") {
|
||||
// If the user is on the home page, we should stop the socket connection.
|
||||
// This is relevant when the user redirects here for whatever reason.
|
||||
if (isConnected) stop();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleUserLogout = () => {
|
||||
logoutFetcher.submit(
|
||||
{},
|
||||
@@ -313,11 +303,9 @@ export default function MainApp() {
|
||||
<p className="text-xs text-[#A3A3A3]">
|
||||
To continue, connect an OpenAI, Anthropic, or other LLM account
|
||||
</p>
|
||||
{isConnected && (
|
||||
<p className="text-xs text-danger">
|
||||
Changing settings during an active session will end the session
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-danger">
|
||||
Changing settings during an active session will end the session
|
||||
</p>
|
||||
<SettingsForm
|
||||
settings={settings}
|
||||
models={settingsFormData.models}
|
||||
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
import { setCurStatusMessage } from "#/state/statusSlice";
|
||||
import store from "#/store";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import { ActionMessage, StatusMessage } from "#/types/Message";
|
||||
import { SocketMessage } from "#/types/ResponseType";
|
||||
import {
|
||||
ActionMessage,
|
||||
ObservationMessage,
|
||||
StatusMessage,
|
||||
} from "#/types/Message";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
|
||||
const messageActions = {
|
||||
@@ -138,22 +141,14 @@ export function handleStatusMessage(message: StatusMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAssistantMessage(data: string | SocketMessage) {
|
||||
let socketMessage: SocketMessage;
|
||||
|
||||
if (typeof data === "string") {
|
||||
socketMessage = JSON.parse(data) as SocketMessage;
|
||||
export function handleAssistantMessage(message: Record<string, unknown>) {
|
||||
if (message.action) {
|
||||
handleActionMessage(message as unknown as ActionMessage);
|
||||
} else if (message.observation) {
|
||||
handleObservationMessage(message as unknown as ObservationMessage);
|
||||
} else if (message.status_update) {
|
||||
handleStatusMessage(message as unknown as StatusMessage);
|
||||
} else {
|
||||
socketMessage = data;
|
||||
}
|
||||
|
||||
if ("action" in socketMessage) {
|
||||
handleActionMessage(socketMessage);
|
||||
} else if ("observation" in socketMessage) {
|
||||
handleObservationMessage(socketMessage);
|
||||
} else if ("status_update" in socketMessage) {
|
||||
handleStatusMessage(socketMessage);
|
||||
} else {
|
||||
console.error("Unknown message type", socketMessage);
|
||||
console.error("Unknown message type", message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import ActionType from "#/types/ActionType";
|
||||
import AgentState from "#/types/AgentState";
|
||||
|
||||
export const generateAgentStateChangeEvent = (state: AgentState) =>
|
||||
JSON.stringify({
|
||||
action: ActionType.CHANGE_AGENT_STATE,
|
||||
args: { agent_state: state },
|
||||
});
|
||||
export const generateAgentStateChangeEvent = (state: AgentState) => ({
|
||||
action: ActionType.CHANGE_AGENT_STATE,
|
||||
args: { agent_state: state },
|
||||
});
|
||||
|
||||
@@ -9,5 +9,5 @@ export function createChatMessage(
|
||||
action: ActionType.MESSAGE,
|
||||
args: { content: message, images_urls, timestamp },
|
||||
};
|
||||
return JSON.stringify(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import ActionType from "#/types/ActionType";
|
||||
|
||||
export function getTerminalCommand(command: string, hidden: boolean = false) {
|
||||
const event = { action: ActionType.RUN, args: { command, hidden } };
|
||||
return JSON.stringify(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function getGitHubTokenCommand(gitHubToken: string) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export const VERIFIED_ANTHROPIC_MODELS = [
|
||||
"claude-2",
|
||||
"claude-2.1",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { RenderOptions, render } from "@testing-library/react";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { SocketProvider } from "#/context/socket";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
|
||||
const setupStore = (preloadedState?: Partial<RootState>): AppStore =>
|
||||
configureStore({
|
||||
@@ -35,7 +35,7 @@ export function renderWithProviders(
|
||||
function Wrapper({ children }: PropsWithChildren<object>): JSX.Element {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<SocketProvider>{children}</SocketProvider>
|
||||
<WsClientProvider enabled={true} token={null} ghToken={null} settings={null}>{children}</WsClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import os
|
||||
import traceback
|
||||
from typing import Callable, ClassVar, Type
|
||||
|
||||
@@ -259,7 +260,11 @@ class AgentController:
|
||||
observation_to_print.content = truncate_content(
|
||||
observation_to_print.content, self.agent.llm.config.max_message_chars
|
||||
)
|
||||
self.log('debug', str(observation_to_print), extra={'msg_type': 'OBSERVATION'})
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
self.log(
|
||||
log_level, str(observation_to_print), extra={'msg_type': 'OBSERVATION'}
|
||||
)
|
||||
|
||||
if observation.llm_metrics is not None:
|
||||
self.agent.llm.metrics.merge(observation.llm_metrics)
|
||||
@@ -282,8 +287,12 @@ class AgentController:
|
||||
action (MessageAction): The message action to handle.
|
||||
"""
|
||||
if action.source == EventSource.USER:
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = (
|
||||
'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
)
|
||||
self.log(
|
||||
'debug',
|
||||
log_level,
|
||||
str(action),
|
||||
extra={'msg_type': 'ACTION', 'event_source': EventSource.USER},
|
||||
)
|
||||
@@ -497,7 +506,9 @@ class AgentController:
|
||||
|
||||
await self.update_state_after_step()
|
||||
|
||||
self.log('debug', str(action), extra={'msg_type': 'ACTION'})
|
||||
# Use info level if LOG_ALL_EVENTS is set
|
||||
log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
|
||||
self.log(log_level, str(action), extra={'msg_type': 'ACTION'})
|
||||
|
||||
async def _delegate_step(self):
|
||||
"""Executes a single step of the delegate agent."""
|
||||
@@ -663,7 +674,7 @@ class AgentController:
|
||||
# sanity check
|
||||
if start_id > end_id + 1:
|
||||
self.log(
|
||||
'debug',
|
||||
'warning',
|
||||
f'start_id {start_id} is greater than end_id + 1 ({end_id + 1}). History will be empty.',
|
||||
)
|
||||
self.state.history = []
|
||||
@@ -694,7 +705,7 @@ class AgentController:
|
||||
# Match with most recent unmatched delegate action
|
||||
if not delegate_action_ids:
|
||||
self.log(
|
||||
'error',
|
||||
'warning',
|
||||
f'Found AgentDelegateObservation without matching action at id={event.id}',
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -177,7 +177,7 @@ class SensitiveDataFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
def get_console_handler(log_level=logging.INFO, extra_info: str | None = None):
|
||||
def get_console_handler(log_level: int = logging.INFO, extra_info: str | None = None):
|
||||
"""Returns a console handler for logging."""
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(log_level)
|
||||
@@ -188,7 +188,7 @@ def get_console_handler(log_level=logging.INFO, extra_info: str | None = None):
|
||||
return console_handler
|
||||
|
||||
|
||||
def get_file_handler(log_dir, log_level=logging.INFO):
|
||||
def get_file_handler(log_dir: str, log_level: int = logging.INFO):
|
||||
"""Returns a file handler for logging."""
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
@@ -137,7 +137,7 @@ class RemoteRuntime(Runtime):
|
||||
try:
|
||||
response = self._send_request(
|
||||
'GET',
|
||||
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.sid}',
|
||||
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
|
||||
timeout=5,
|
||||
)
|
||||
except requests.HTTPError as e:
|
||||
@@ -227,7 +227,7 @@ class RemoteRuntime(Runtime):
|
||||
'command': command,
|
||||
'working_dir': '/openhands/code/',
|
||||
'environment': {'DEBUG': 'true'} if self.config.debug else {},
|
||||
'runtime_id': self.sid,
|
||||
'session_id': self.sid,
|
||||
}
|
||||
|
||||
# Start the sandbox using the /start endpoint
|
||||
@@ -276,7 +276,7 @@ class RemoteRuntime(Runtime):
|
||||
self.log('debug', f'Waiting for runtime to be alive at url: {self.runtime_url}')
|
||||
runtime_info_response = self._send_request(
|
||||
'GET',
|
||||
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
|
||||
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
|
||||
)
|
||||
runtime_data = runtime_info_response.json()
|
||||
assert 'runtime_id' in runtime_data
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from github import Github
|
||||
from github.GithubException import GithubException
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.sheets_client import GoogleSheetsClient
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID', '').strip()
|
||||
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '').strip()
|
||||
@@ -12,7 +14,7 @@ GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '').strip()
|
||||
|
||||
class UserVerifier:
|
||||
def __init__(self) -> None:
|
||||
logger.info('Initializing UserVerifier')
|
||||
logger.debug('Initializing UserVerifier')
|
||||
self.file_users: list[str] | None = None
|
||||
self.sheets_client: GoogleSheetsClient | None = None
|
||||
self.spreadsheet_id: str | None = None
|
||||
@@ -25,7 +27,7 @@ class UserVerifier:
|
||||
"""Load users from text file if configured"""
|
||||
waitlist = os.getenv('GITHUB_USER_LIST_FILE')
|
||||
if not waitlist:
|
||||
logger.info('GITHUB_USER_LIST_FILE not configured')
|
||||
logger.debug('GITHUB_USER_LIST_FILE not configured')
|
||||
return
|
||||
|
||||
if not os.path.exists(waitlist):
|
||||
@@ -46,10 +48,10 @@ class UserVerifier:
|
||||
sheet_id = os.getenv('GITHUB_USERS_SHEET_ID')
|
||||
|
||||
if not sheet_id:
|
||||
logger.info('GITHUB_USERS_SHEET_ID not configured')
|
||||
logger.debug('GITHUB_USERS_SHEET_ID not configured')
|
||||
return
|
||||
|
||||
logger.info('Initializing Google Sheets integration')
|
||||
logger.debug('Initializing Google Sheets integration')
|
||||
self.sheets_client = GoogleSheetsClient()
|
||||
self.spreadsheet_id = sheet_id
|
||||
|
||||
@@ -61,21 +63,21 @@ class UserVerifier:
|
||||
if not self.is_active():
|
||||
return True
|
||||
|
||||
logger.info(f'Checking if GitHub user {username} is allowed')
|
||||
logger.debug(f'Checking if GitHub user {username} is allowed')
|
||||
if self.file_users:
|
||||
if username in self.file_users:
|
||||
logger.info(f'User {username} found in text file allowlist')
|
||||
logger.debug(f'User {username} found in text file allowlist')
|
||||
return True
|
||||
logger.debug(f'User {username} not found in text file allowlist')
|
||||
|
||||
if self.sheets_client and self.spreadsheet_id:
|
||||
sheet_users = self.sheets_client.get_usernames(self.spreadsheet_id)
|
||||
if username in sheet_users:
|
||||
logger.info(f'User {username} found in Google Sheets allowlist')
|
||||
logger.debug(f'User {username} found in Google Sheets allowlist')
|
||||
return True
|
||||
logger.debug(f'User {username} not found in Google Sheets allowlist')
|
||||
|
||||
logger.info(f'User {username} not found in any allowlist')
|
||||
logger.debug(f'User {username} not found in any allowlist')
|
||||
return False
|
||||
|
||||
|
||||
@@ -83,10 +85,10 @@ async def authenticate_github_user(auth_token) -> bool:
|
||||
user_verifier = UserVerifier()
|
||||
|
||||
if not user_verifier.is_active():
|
||||
logger.info('No user verification sources configured - allowing all users')
|
||||
logger.debug('No user verification sources configured - allowing all users')
|
||||
return True
|
||||
|
||||
logger.info('Checking GitHub token')
|
||||
logger.debug('Checking GitHub token')
|
||||
|
||||
if not auth_token:
|
||||
logger.warning('No GitHub token provided')
|
||||
@@ -112,25 +114,14 @@ async def get_github_user(token: str) -> str:
|
||||
Returns:
|
||||
github handle of the user
|
||||
"""
|
||||
logger.info('Fetching GitHub user info from token')
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'Authorization': f'Bearer {token}',
|
||||
}
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=5.0)
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get('https://api.github.com/user', headers=headers)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Error making request to GitHub API: {str(e)}')
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
logger.info('Received response from GitHub API')
|
||||
logger.debug(f'Response status code: {response.status_code}')
|
||||
response.raise_for_status()
|
||||
user_data = response.json()
|
||||
login = user_data.get('login')
|
||||
logger.debug('Fetching GitHub user info from token')
|
||||
try:
|
||||
g = Github(token)
|
||||
user = await call_sync_from_async(g.get_user)
|
||||
login = user.login
|
||||
logger.info(f'Successfully retrieved GitHub user: {login}')
|
||||
return login
|
||||
except GithubException as e:
|
||||
logger.error(f'Error making request to GitHub API: {str(e)}')
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
52
poetry.lock
generated
@@ -6778,6 +6778,25 @@ files = [
|
||||
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygithub"
|
||||
version = "2.5.0"
|
||||
description = "Use the full Github API v3"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"},
|
||||
{file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Deprecated = "*"
|
||||
pyjwt = {version = ">=2.4.0", extras = ["crypto"]}
|
||||
pynacl = ">=1.4.0"
|
||||
requests = ">=2.14.0"
|
||||
typing-extensions = ">=4.0.0"
|
||||
urllib3 = ">=1.26.0"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.18.0"
|
||||
@@ -6842,6 +6861,32 @@ files = [
|
||||
[package.dependencies]
|
||||
pybind11 = ">=2.2"
|
||||
|
||||
[[package]]
|
||||
name = "pynacl"
|
||||
version = "1.5.0"
|
||||
description = "Python binding to the Networking and Cryptography (NaCl) library"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
|
||||
{file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.4.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.0"
|
||||
@@ -7995,6 +8040,11 @@ files = [
|
||||
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"},
|
||||
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"},
|
||||
{file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"},
|
||||
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"},
|
||||
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"},
|
||||
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"},
|
||||
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"},
|
||||
{file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"},
|
||||
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"},
|
||||
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"},
|
||||
{file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"},
|
||||
@@ -10128,4 +10178,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "8a34ef6158ca2a9fe3615fc362db3fd71bc43eabb57ffc2e2e14dfb658cf52c3"
|
||||
content-hash = "245fd4cd56a3c95b2dd4f3a06251f7de82ad0300de7349f0710aac1f92a151b7"
|
||||
|
||||
@@ -62,6 +62,7 @@ opentelemetry-api = "1.25.0"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
|
||||
modal = "^0.64.145"
|
||||
runloop-api-client = "0.7.0"
|
||||
pygithub = "^2.5.0"
|
||||
|
||||
[tool.poetry.group.llama-index.dependencies]
|
||||
llama-index = "*"
|
||||
@@ -93,6 +94,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -123,6 +125,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||