mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-11 07:15:08 -05:00
Complete the implementation of the Agent Run Scheduling UX in the Library. Demo: https://github.com/user-attachments/assets/701adc63-452c-4d37-aeea-51788b2774f2 ### Changes 🏗️ Frontend: - Add "Schedule" button + dialog + logic to `AgentRunDraftView` - Update corresponding logic on `AgentRunsPage` - Add schedule name field to `CronSchedulerDialog` - Amend Builder components `useAgentGraph`, `FlowEditor`, `RunnerUIWrapper` to also handle schedule name input - Split `CronScheduler` into `CronScheduler`+`CronSchedulerDialog` - Make `AgentScheduleDetailsView` more fully functional - Add schedule description to info box - Add "Delete schedule" button - Update schedule create/select/delete logic in `AgentRunsPage` - Improve schedule UX in `AgentRunsSelectorList` - Switch tabs automatically when a run or schedule is selected - Remove now-redundant schedule filters - Refactor `@/lib/monitor/cronExpressionManager` into `@/lib/cron-expression-utils` Backend + API: - Add name and credentials to graph execution schedule job params - Update schedule API - `POST /schedules` -> `POST /graphs/{graph_id}/schedules` - Add `GET /graphs/{graph_id}/schedules` - Add not found error handling to `DELETE /schedules/{schedule_id}` - Minor refactoring Backend: - Fix "`GraphModel`->`NodeModel` is not fully defined" error in scheduler - Add support for all exceptions defined in `backend.util.exceptions` to RPC logic in `backend.util.service` - Fix inconsistent log prefixing in `backend.executor.scheduler` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - Create a simple agent with inputs and blocks that require credentials; go to this agent in the Library - Fill out the inputs and click "Schedule"; make it run every minute (for testing purposes) - [x] -> newly created schedule appears in the list - [x] -> scheduled runs are successful - Click "Delete schedule" - [x] -> schedule no longer in list - [x] -> on deleting the last schedule, view switches back to the Runs list - [x] -> no new runs occur from the deleted schedule
222 lines
6.0 KiB
TypeScript
222 lines
6.0 KiB
TypeScript
import React, {
|
|
useState,
|
|
useCallback,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
} from "react";
|
|
import RunnerOutputUI, { BlockOutput } from "./runner-ui/RunnerOutputUI";
|
|
import RunnerInputUI from "./runner-ui/RunnerInputUI";
|
|
import { Node } from "@xyflow/react";
|
|
import { filterBlocksByType } from "@/lib/utils";
|
|
import {
|
|
BlockIOObjectSubSchema,
|
|
BlockIORootSchema,
|
|
BlockUIType,
|
|
} from "@/lib/autogpt-server-api/types";
|
|
import { CustomNode } from "./CustomNode";
|
|
|
|
interface HardcodedValues {
|
|
name: any;
|
|
description: any;
|
|
value: any;
|
|
placeholder_values: any;
|
|
}
|
|
|
|
export interface InputItem {
|
|
id: string;
|
|
type: "input";
|
|
inputSchema: BlockIORootSchema;
|
|
hardcodedValues: HardcodedValues;
|
|
}
|
|
|
|
interface RunnerUIWrapperProps {
|
|
nodes: Node[];
|
|
setNodes: React.Dispatch<React.SetStateAction<CustomNode[]>>;
|
|
setIsScheduling: React.Dispatch<React.SetStateAction<boolean>>;
|
|
isRunning: boolean;
|
|
isScheduling: boolean;
|
|
requestSaveAndRun: () => void;
|
|
scheduleRunner: (
|
|
cronExpression: string,
|
|
input: InputItem[],
|
|
scheduleName: string,
|
|
) => Promise<void>;
|
|
}
|
|
|
|
export interface RunnerUIWrapperRef {
|
|
openRunnerInput: () => void;
|
|
openRunnerOutput: () => void;
|
|
runOrOpenInput: () => void;
|
|
collectInputsForScheduling: (
|
|
cronExpression: string,
|
|
scheduleName: string,
|
|
) => void;
|
|
}
|
|
|
|
const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
|
(
|
|
{
|
|
nodes,
|
|
setIsScheduling,
|
|
setNodes,
|
|
isScheduling,
|
|
isRunning,
|
|
requestSaveAndRun,
|
|
scheduleRunner,
|
|
},
|
|
ref,
|
|
) => {
|
|
const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false);
|
|
const [isRunnerOutputOpen, setIsRunnerOutputOpen] = useState(false);
|
|
const [scheduledInput, setScheduledInput] = useState(false);
|
|
const [cronExpression, setCronExpression] = useState("");
|
|
const [scheduleName, setScheduleName] = useState("");
|
|
|
|
const getBlockInputsAndOutputs = useCallback((): {
|
|
inputs: InputItem[];
|
|
outputs: BlockOutput[];
|
|
} => {
|
|
const inputBlocks = filterBlocksByType(
|
|
nodes,
|
|
(node) => node.data.uiType === BlockUIType.INPUT,
|
|
);
|
|
|
|
const outputBlocks = filterBlocksByType(
|
|
nodes,
|
|
(node) => node.data.uiType === BlockUIType.OUTPUT,
|
|
);
|
|
|
|
const inputs = inputBlocks.map(
|
|
(node) =>
|
|
({
|
|
id: node.id,
|
|
type: "input" as const,
|
|
inputSchema: (node.data.inputSchema as BlockIOObjectSubSchema)
|
|
.properties.value as BlockIORootSchema,
|
|
hardcodedValues: {
|
|
name: (node.data.hardcodedValues as any).name || "",
|
|
description: (node.data.hardcodedValues as any).description || "",
|
|
value: (node.data.hardcodedValues as any).value,
|
|
placeholder_values:
|
|
(node.data.hardcodedValues as any).placeholder_values || [],
|
|
},
|
|
}) satisfies InputItem,
|
|
);
|
|
|
|
const outputs = outputBlocks.map(
|
|
(node) =>
|
|
({
|
|
metadata: {
|
|
name: (node.data.hardcodedValues as any).name || "Output",
|
|
description:
|
|
(node.data.hardcodedValues as any).description ||
|
|
"Output from the agent",
|
|
},
|
|
result:
|
|
(node.data.executionResults as any)
|
|
?.map((result: any) => result?.data?.output)
|
|
.join("\n--\n") || "No output yet",
|
|
}) satisfies BlockOutput,
|
|
);
|
|
|
|
return { inputs, outputs };
|
|
}, [nodes]);
|
|
|
|
const handleInputChange = useCallback(
|
|
(nodeId: string, field: string, value: any) => {
|
|
setNodes((nds) =>
|
|
nds.map((node) => {
|
|
if (node.id === nodeId) {
|
|
return {
|
|
...node,
|
|
data: {
|
|
...node.data,
|
|
hardcodedValues: {
|
|
...(node.data.hardcodedValues as any),
|
|
[field]: value,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return node;
|
|
}),
|
|
);
|
|
},
|
|
[setNodes],
|
|
);
|
|
|
|
const openRunnerInput = () => setIsRunnerInputOpen(true);
|
|
const openRunnerOutput = () => setIsRunnerOutputOpen(true);
|
|
|
|
const runOrOpenInput = () => {
|
|
const { inputs } = getBlockInputsAndOutputs();
|
|
if (inputs.length > 0) {
|
|
openRunnerInput();
|
|
} else {
|
|
requestSaveAndRun();
|
|
}
|
|
};
|
|
|
|
const collectInputsForScheduling = (
|
|
cronExpression: string,
|
|
scheduleName: string,
|
|
) => {
|
|
const { inputs } = getBlockInputsAndOutputs();
|
|
setCronExpression(cronExpression);
|
|
setScheduleName(scheduleName);
|
|
|
|
if (inputs.length > 0) {
|
|
setScheduledInput(true);
|
|
setIsRunnerInputOpen(true);
|
|
} else {
|
|
scheduleRunner(cronExpression, [], scheduleName);
|
|
}
|
|
};
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
openRunnerInput,
|
|
openRunnerOutput,
|
|
runOrOpenInput,
|
|
collectInputsForScheduling,
|
|
}));
|
|
|
|
return (
|
|
<>
|
|
<RunnerInputUI
|
|
isOpen={isRunnerInputOpen}
|
|
onClose={() => setIsRunnerInputOpen(false)}
|
|
blockInputs={getBlockInputsAndOutputs().inputs}
|
|
onInputChange={handleInputChange}
|
|
onRun={() => {
|
|
setIsRunnerInputOpen(false);
|
|
requestSaveAndRun();
|
|
}}
|
|
scheduledInput={scheduledInput}
|
|
isScheduling={isScheduling}
|
|
onSchedule={async () => {
|
|
setIsScheduling(true);
|
|
await scheduleRunner(
|
|
cronExpression,
|
|
getBlockInputsAndOutputs().inputs,
|
|
scheduleName,
|
|
);
|
|
setIsScheduling(false);
|
|
setIsRunnerInputOpen(false);
|
|
setScheduledInput(false);
|
|
}}
|
|
isRunning={isRunning}
|
|
/>
|
|
<RunnerOutputUI
|
|
isOpen={isRunnerOutputOpen}
|
|
onClose={() => setIsRunnerOutputOpen(false)}
|
|
blockOutputs={getBlockInputsAndOutputs().outputs}
|
|
/>
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
RunnerUIWrapper.displayName = "RunnerUIWrapper";
|
|
|
|
export default RunnerUIWrapper;
|