Skip to content

Commit

Permalink
[editor] Server Status Heartbeat (#762)
Browse files Browse the repository at this point in the history
[editor] Server Status Heartbeat

# [editor] Server Status Heartbeat

Implement a `server_status` endpoint to use as a heartbeat for the
client to know if the server is down; if the request fails, we'll show
an error banner at the top of the config to warn the user that the
server's in a bad state & some remediation steps (copy important work &
restart editor)

<img width="1053" alt="Screenshot 2024-01-04 at 6 58 48 PM"
src="https://github.com/lastmile-ai/aiconfig/assets/5060851/ddb3284b-6d2b-4441-bed5-8b747dcc4268">

QUESTION: Once the server status is marked as error, do we want to keep
the heartbeat going (is it possible for the server to recover without
user manually restarting it?)


## Testing:
- Load editor, ensure good status and no banner shown
- CMD+C in CLI to shut down the editor. Make sure client shows the error
banner
- Start editor again. Client reloads and shows no error
  • Loading branch information
rholinshead authored Jan 5, 2024
2 parents 056d871 + a0fb40b commit d63d78d
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 23 deletions.
6 changes: 6 additions & 0 deletions python/src/aiconfig/editor/client/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,16 @@ export default function Editor() {
[]
);

const getServerStatus = useCallback(async () => {
return await ufetch.get(ROUTE_TABLE.SERVER_STATUS);
}, []);

const callbacks: AIConfigCallbacks = useMemo(
() => ({
addPrompt,
deletePrompt,
getModels,
getServerStatus,
runPrompt,
save,
setConfigDescription,
Expand All @@ -130,6 +135,7 @@ export default function Editor() {
addPrompt,
deletePrompt,
getModels,
getServerStatus,
runPrompt,
save,
setConfigDescription,
Expand Down
28 changes: 28 additions & 0 deletions python/src/aiconfig/editor/client/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
CopyButton as MantineCopyButton,
ActionIcon,
Tooltip,
} from "@mantine/core";
import { IconCheck, IconCopy } from "@tabler/icons-react";

type Props = {
value: string;
contentLabel?: string;
};
export default function CopyButton({ value, contentLabel }: Props) {
const labelSuffix = contentLabel ? ` ${contentLabel}` : "";
return (
<MantineCopyButton value={value} timeout={2000}>
{({ copied, copy }) => (
<Tooltip
label={copied ? `Copied${labelSuffix}` : `Copy${labelSuffix}`}
withArrow
>
<ActionIcon color={copied ? "teal" : "gray"} onClick={copy}>
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
</ActionIcon>
</Tooltip>
)}
</MantineCopyButton>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
createStyles,
Stack,
Flex,
Text,
Tooltip,
Alert,
} from "@mantine/core";
import { Notifications, showNotification } from "@mantine/notifications";
import {
Expand Down Expand Up @@ -40,9 +42,14 @@ import PromptMenuButton from "./prompt/PromptMenuButton";
import GlobalParametersContainer from "./GlobalParametersContainer";
import AIConfigContext from "./AIConfigContext";
import ConfigNameDescription from "./ConfigNameDescription";
import { AUTOSAVE_INTERVAL_MS, DEBOUNCE_MS } from "../utils/constants";
import {
AUTOSAVE_INTERVAL_MS,
DEBOUNCE_MS,
SERVER_HEARTBEAT_INTERVAL_MS,
} from "../utils/constants";
import { getPromptModelName } from "../utils/promptUtils";
import { IconDeviceFloppy } from "@tabler/icons-react";
import CopyButton from "./CopyButton";

type Props = {
aiconfig: AIConfig;
Expand All @@ -57,6 +64,7 @@ export type AIConfigCallbacks = {
) => Promise<{ aiconfig: AIConfig }>;
deletePrompt: (promptName: string) => Promise<void>;
getModels: (search: string) => Promise<string[]>;
getServerStatus?: () => Promise<{ status: "OK" | "ERROR" }>;
runPrompt: (promptName: string) => Promise<{ aiconfig: AIConfig }>;
save: (aiconfig: AIConfig) => Promise<void>;
setConfigDescription: (description: string) => Promise<void>;
Expand Down Expand Up @@ -110,6 +118,7 @@ export default function EditorContainer({
callbacks,
}: Props) {
const [isSaving, setIsSaving] = useState(false);
const [serverStatus, setServerStatus] = useState<"OK" | "ERROR">("OK");
const [aiconfigState, dispatch] = useReducer(
aiconfigReducer,
aiConfigToClientConfig(initialAIConfig)
Expand Down Expand Up @@ -668,9 +677,58 @@ export default function EditorContainer({
return () => window.removeEventListener("keydown", saveHandler);
}, [onSave]);

// Server heartbeat, check every 3s to show error if server is down
// Don't poll if server status is in an error state since it won't automatically recover
const getServerStatusCallback = callbacks.getServerStatus;
useEffect(() => {
if (!getServerStatusCallback || serverStatus !== "OK") {
return;
}

const interval = setInterval(async () => {
try {
const res = await getServerStatusCallback();
setServerStatus(res.status);
} catch (err: unknown) {
setServerStatus("ERROR");
}
}, SERVER_HEARTBEAT_INTERVAL_MS);

return () => clearInterval(interval);
}, [getServerStatusCallback, serverStatus]);

return (
<AIConfigContext.Provider value={contextValue}>
<Notifications />
{serverStatus !== "OK" && (
<>
{/* // Simple placeholder block div to make sure the banner does not overlap page contents until scrolling past its height */}
<div style={{ height: "100px" }} />
<Alert
color="red"
title="Server Connection Error"
w="100%"
style={{ position: "fixed", top: 0, zIndex: 999 }}
>
<Text>
There is a problem with the editor server connection. Please copy
important changes somewhere safe and then try reloading the page
or restarting the editor.
</Text>
<Flex align="center">
<CopyButton
value={JSON.stringify(
clientConfigToAIConfig(aiconfigState),
null,
2
)}
contentLabel="AIConfig JSON"
/>
<Text color="dimmed">Click to copy current AIConfig JSON</Text>
</Flex>
</Alert>
</>
)}
<Container maw="80rem">
<Flex justify="flex-end" mt="md" mb="xs">
<Tooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { ActionIcon, CopyButton, Flex, Tooltip } from "@mantine/core";
import {
IconBraces,
IconBracesOff,
IconCheck,
IconCopy,
} from "@tabler/icons-react";
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconBraces, IconBracesOff } from "@tabler/icons-react";
import { Output } from "aiconfig";
import { memo, useState } from "react";
import JSONRenderer from "../../JSONRenderer";
import CopyButton from "../../CopyButton";

type Props = {
children: React.ReactNode;
Expand All @@ -26,21 +22,7 @@ export default memo(function PromptOutputWrapper({
return (
<>
<Flex justify="flex-end">
{copyContent && (
<CopyButton value={copyContent} timeout={2000}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"} withArrow>
<ActionIcon color={copied ? "teal" : "gray"} onClick={copy}>
{copied ? (
<IconCheck size="1rem" />
) : (
<IconCopy size="1rem" />
)}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
)}
{copyContent && <CopyButton value={copyContent} />}
{withRawJSONToggle && (
<Tooltip label="Toggle raw JSON" withArrow>
<ActionIcon onClick={() => setIsRawJSON((curr) => !curr)}>
Expand Down
1 change: 1 addition & 0 deletions python/src/aiconfig/editor/client/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ROUTE_TABLE = {
DELETE_PROMPT: urlJoin(API_ENDPOINT, "/delete_prompt"),
SAVE: urlJoin(API_ENDPOINT, "/save"),
SET_DESCRIPTION: urlJoin(API_ENDPOINT, "/set_description"),
SERVER_STATUS: urlJoin(API_ENDPOINT, "/server_status"),
SET_NAME: urlJoin(API_ENDPOINT, "/set_name"),
SET_PARAMETERS: urlJoin(API_ENDPOINT, "/set_parameters"),
LOAD: urlJoin(API_ENDPOINT, "/load"),
Expand Down
1 change: 1 addition & 0 deletions python/src/aiconfig/editor/client/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const DEBOUNCE_MS = 300;
export const AUTOSAVE_INTERVAL_MS = 15 * 1000; // 15 seconds
export const SERVER_HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds
5 changes: 5 additions & 0 deletions python/src/aiconfig/editor/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def home():
return app.send_static_file("index.html")


@app.route("/api/server_status", methods=["GET"])
def server_status() -> FlaskResponse:
return FlaskResponse(({"status": "OK"}, 200))


@app.route("/api/list_models", methods=["GET"])
def list_models() -> FlaskResponse:
out: list[str] = ModelParserRegistry.parser_ids() # type: ignore
Expand Down

0 comments on commit d63d78d

Please sign in to comment.