mirror of
https://github.com/Significant-Gravitas/Auto-GPT.git
synced 2025-01-08 11:57:32 +08:00
feat(platform) : scheduling agent runner (#8634)
* add: ui for scheduling agent * adding requests and type for schedule endpoints * feat : monitor schdules on monitor page * add: Complete monitor page * fix filter on monitor page * fix linting * PR nits * Added Docker Compose env var --------- Co-authored-by: Toran Bruce Richards <toran.richards@gmail.com> Co-authored-by: Swifty <craigswift13@gmail.com> Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
This commit is contained in:
parent
bbbdb5665b
commit
dd0081ab35
@ -37,7 +37,7 @@ class ExecutionSchedule(BaseDbModel):
|
||||
|
||||
async def get_active_schedules(last_fetch_time: datetime) -> list[ExecutionSchedule]:
|
||||
query = AgentGraphExecutionSchedule.prisma().find_many(
|
||||
where={"isEnabled": True, "lastUpdated": {"gt": last_fetch_time}},
|
||||
where={"lastUpdated": {"gt": last_fetch_time}},
|
||||
order={"lastUpdated": "asc"},
|
||||
)
|
||||
return [ExecutionSchedule.from_db(schedule) for schedule in await query]
|
||||
|
@ -65,6 +65,7 @@ services:
|
||||
- REDIS_PASSWORD=password
|
||||
- ENABLE_AUTH=true
|
||||
- PYRO_HOST=0.0.0.0
|
||||
- EXECUTIONSCHEDULER_HOST=rest_server
|
||||
- EXECUTIONMANAGER_HOST=executor
|
||||
- DATABASEMANAGER_HOST=executor
|
||||
- FRONTEND_BASE_URL=http://localhost:3000
|
||||
|
@ -34,6 +34,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import AutoGPTServerAPI, {
|
||||
GraphMetaWithRuns,
|
||||
ExecutionMeta,
|
||||
Schedule,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
@ -15,17 +16,51 @@ import {
|
||||
FlowRunsList,
|
||||
FlowRunsStats,
|
||||
} from "@/components/monitor";
|
||||
import { SchedulesTable } from "@/components/monitor/scheduleTable";
|
||||
|
||||
const Monitor = () => {
|
||||
const [flows, setFlows] = useState<GraphMetaWithRuns[]>([]);
|
||||
const [flowRuns, setFlowRuns] = useState<FlowRun[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [selectedFlow, setSelectedFlow] = useState<GraphMetaWithRuns | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
|
||||
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
|
||||
const api = useMemo(() => new AutoGPTServerAPI(), []);
|
||||
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
const schedulesData: Schedule[] = [];
|
||||
for (const flow of flows) {
|
||||
const flowSchedules = await api.getSchedules(flow.id);
|
||||
Object.entries(flowSchedules).forEach(([id, schedule]) => {
|
||||
schedulesData.push({
|
||||
id,
|
||||
schedule,
|
||||
graph_id: flow.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setSchedules(schedulesData);
|
||||
}, [api, flows]);
|
||||
|
||||
const toggleSchedule = useCallback(
|
||||
async (scheduleId: string, enabled: boolean) => {
|
||||
await api.updateSchedule(scheduleId, { is_enabled: enabled });
|
||||
setSchedules((prevSchedules) =>
|
||||
prevSchedules.map((schedule) =>
|
||||
schedule.id === scheduleId
|
||||
? { ...schedule, isEnabled: enabled }
|
||||
: schedule,
|
||||
),
|
||||
);
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
api.listGraphsWithRuns().then((agent) => {
|
||||
setFlows(agent);
|
||||
@ -44,15 +79,28 @@ const Monitor = () => {
|
||||
fetchAgents();
|
||||
}, [api, fetchAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchedules();
|
||||
}, [fetchSchedules, flows]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => fetchAgents(), 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchAgents, flows]);
|
||||
|
||||
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
|
||||
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3 space-y-4";
|
||||
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3";
|
||||
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
|
||||
|
||||
const handleSort = (column: keyof Schedule) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10">
|
||||
<AgentFlowList
|
||||
@ -101,6 +149,16 @@ const Monitor = () => {
|
||||
<FlowRunsStats flows={flows} flowRuns={flowRuns} />
|
||||
</Card>
|
||||
)}
|
||||
<div className="col-span-full md:col-span-3 lg:col-span-2 xl:col-span-6">
|
||||
<SchedulesTable
|
||||
schedules={schedules} // all schedules
|
||||
agents={flows} // for filtering purpose
|
||||
onToggleSchedule={toggleSchedule}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -45,6 +45,7 @@ import RunnerUIWrapper, {
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||
import { CronScheduler } from "./cronScheduler";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
@ -97,7 +98,10 @@ const FlowEditor: React.FC<{
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
requestStopRun,
|
||||
scheduleRunner,
|
||||
isRunning,
|
||||
isScheduling,
|
||||
setIsScheduling,
|
||||
nodes,
|
||||
setNodes,
|
||||
edges,
|
||||
@ -119,6 +123,8 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
||||
|
||||
const [openCron, setOpenCron] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const TUTORIAL_STORAGE_KEY = "shepherd-tour";
|
||||
@ -146,6 +152,12 @@ const FlowEditor: React.FC<{
|
||||
nodes.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.get("open_scheduling") === "true") {
|
||||
setOpenCron(true);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
@ -611,6 +623,24 @@ const FlowEditor: React.FC<{
|
||||
},
|
||||
];
|
||||
|
||||
// This function is called after cron expression is created
|
||||
// So you can collect inputs for scheduling
|
||||
const afterCronCreation = (cronExpression: string) => {
|
||||
runnerUIRef.current?.collectInputsForScheduling(cronExpression);
|
||||
};
|
||||
|
||||
// This function Opens up form for creating cron expression
|
||||
const handleScheduleButton = () => {
|
||||
if (!savedAgent) {
|
||||
toast({
|
||||
title: `Please save the agent using the button in the left sidebar before running it.`,
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setOpenCron(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<FlowContext.Provider
|
||||
value={{ visualizeBeads, setIsAnyModalOpen, getNextNodeId }}
|
||||
@ -673,18 +703,28 @@ const FlowEditor: React.FC<{
|
||||
requestStopRun();
|
||||
}
|
||||
}}
|
||||
onClickScheduleButton={handleScheduleButton}
|
||||
isScheduling={isScheduling}
|
||||
isDisabled={!savedAgent}
|
||||
isRunning={isRunning}
|
||||
requestStopRun={requestStopRun}
|
||||
runAgentTooltip={!isRunning ? "Run Agent" : "Stop Agent"}
|
||||
/>
|
||||
<CronScheduler
|
||||
afterCronCreation={afterCronCreation}
|
||||
open={openCron}
|
||||
setOpen={setOpenCron}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<RunnerUIWrapper
|
||||
ref={runnerUIRef}
|
||||
nodes={nodes}
|
||||
setNodes={setNodes}
|
||||
setIsScheduling={setIsScheduling}
|
||||
isScheduling={isScheduling}
|
||||
isRunning={isRunning}
|
||||
scheduleRunner={scheduleRunner}
|
||||
requestSaveAndRun={requestSaveAndRun}
|
||||
/>
|
||||
</FlowContext.Provider>
|
||||
|
@ -1,18 +1,21 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { Clock, LogOut, ChevronLeft } from "lucide-react";
|
||||
import { IconPlay, IconSquare } from "@/components/ui/icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
|
||||
interface PrimaryActionBarProps {
|
||||
onClickAgentOutputs: () => void;
|
||||
onClickRunAgent: () => void;
|
||||
onClickScheduleButton: () => void;
|
||||
isRunning: boolean;
|
||||
isDisabled: boolean;
|
||||
isScheduling: boolean;
|
||||
requestStopRun: () => void;
|
||||
runAgentTooltip: string;
|
||||
}
|
||||
@ -20,8 +23,10 @@ interface PrimaryActionBarProps {
|
||||
const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
onClickAgentOutputs,
|
||||
onClickRunAgent,
|
||||
onClickScheduleButton,
|
||||
isRunning,
|
||||
isDisabled,
|
||||
isScheduling,
|
||||
requestStopRun,
|
||||
runAgentTooltip,
|
||||
}) => {
|
||||
@ -74,6 +79,30 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
<p>{runAgentTooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip key="ScheduleAgent" delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onClickScheduleButton}
|
||||
size="primary"
|
||||
disabled={isScheduling}
|
||||
variant="outline"
|
||||
data-id="primary-action-schedule-agent"
|
||||
>
|
||||
{isScheduling ? (
|
||||
<FaSpinner className="animate-spin" />
|
||||
) : (
|
||||
<Clock className="hidden h-5 w-5 md:flex" />
|
||||
)}
|
||||
<span className="text-sm font-medium md:text-lg">
|
||||
Schedule Run
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Schedule this Agent</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,24 +10,55 @@ import { Node } from "@xyflow/react";
|
||||
import { filterBlocksByType } from "@/lib/utils";
|
||||
import { BlockIORootSchema, BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
interface HardcodedValues {
|
||||
name: any;
|
||||
description: any;
|
||||
value: any;
|
||||
placeholder_values: any;
|
||||
limit_to_placeholder_values: any;
|
||||
}
|
||||
|
||||
export interface InputItem {
|
||||
id: string;
|
||||
type: "input";
|
||||
inputSchema: BlockIORootSchema;
|
||||
hardcodedValues: HardcodedValues;
|
||||
}
|
||||
|
||||
interface RunnerUIWrapperProps {
|
||||
nodes: Node[];
|
||||
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
|
||||
setIsScheduling: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isRunning: boolean;
|
||||
isScheduling: boolean;
|
||||
requestSaveAndRun: () => void;
|
||||
scheduleRunner: (cronExpression: string, input: InputItem[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RunnerUIWrapperRef {
|
||||
openRunnerInput: () => void;
|
||||
openRunnerOutput: () => void;
|
||||
runOrOpenInput: () => void;
|
||||
collectInputsForScheduling: (cronExpression: string) => void;
|
||||
}
|
||||
|
||||
const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
({ nodes, setNodes, isRunning, requestSaveAndRun }, ref) => {
|
||||
(
|
||||
{
|
||||
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 getBlockInputsAndOutputs = useCallback(() => {
|
||||
const inputBlocks = filterBlocksByType(
|
||||
nodes,
|
||||
@ -107,10 +138,23 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const collectInputsForScheduling = (cron_exp: string) => {
|
||||
const { inputs } = getBlockInputsAndOutputs();
|
||||
setCronExpression(cron_exp);
|
||||
|
||||
if (inputs.length > 0) {
|
||||
setScheduledInput(true);
|
||||
setIsRunnerInputOpen(true);
|
||||
} else {
|
||||
scheduleRunner(cron_exp, []);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openRunnerInput,
|
||||
openRunnerOutput,
|
||||
runOrOpenInput,
|
||||
collectInputsForScheduling,
|
||||
}));
|
||||
|
||||
return (
|
||||
@ -124,6 +168,18 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
setIsRunnerInputOpen(false);
|
||||
requestSaveAndRun();
|
||||
}}
|
||||
scheduledInput={scheduledInput}
|
||||
isScheduling={isScheduling}
|
||||
onSchedule={async () => {
|
||||
setIsScheduling(true);
|
||||
await scheduleRunner(
|
||||
cronExpression,
|
||||
getBlockInputsAndOutputs().inputs,
|
||||
);
|
||||
setIsScheduling(false);
|
||||
setIsRunnerInputOpen(false);
|
||||
setScheduledInput(false);
|
||||
}}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
<RunnerOutputUI
|
||||
|
425
autogpt_platform/frontend/src/components/cronScheduler.tsx
Normal file
425
autogpt_platform/frontend/src/components/cronScheduler.tsx
Normal file
@ -0,0 +1,425 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { CronExpressionManager } from "@/lib/monitor/cronExpressionManager";
|
||||
|
||||
interface CronSchedulerProps {
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
open: boolean;
|
||||
afterCronCreation: (cronExpression: string) => void;
|
||||
}
|
||||
|
||||
export function CronScheduler({
|
||||
setOpen,
|
||||
open,
|
||||
afterCronCreation,
|
||||
}: CronSchedulerProps) {
|
||||
const [frequency, setFrequency] = useState<
|
||||
"minute" | "hour" | "daily" | "weekly" | "monthly" | "yearly" | "custom"
|
||||
>("minute");
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
||||
const [selectedTime, setSelectedTime] = useState<string>("00:00");
|
||||
const [showCustomDays, setShowCustomDays] = useState<boolean>(false);
|
||||
const [selectedMinute, setSelectedMinute] = useState<string>("0");
|
||||
const [customInterval, setCustomInterval] = useState<{
|
||||
value: number;
|
||||
unit: "minutes" | "hours" | "days";
|
||||
}>({ value: 1, unit: "minutes" });
|
||||
|
||||
// const [endType, setEndType] = useState<"never" | "on" | "after">("never");
|
||||
// const [endDate, setEndDate] = useState<Date | undefined>();
|
||||
// const [occurrences, setOccurrences] = useState<number>(1);
|
||||
|
||||
const weekDays = [
|
||||
{ label: "Su", value: 0 },
|
||||
{ label: "Mo", value: 1 },
|
||||
{ label: "Tu", value: 2 },
|
||||
{ label: "We", value: 3 },
|
||||
{ label: "Th", value: 4 },
|
||||
{ label: "Fr", value: 5 },
|
||||
{ label: "Sa", value: 6 },
|
||||
];
|
||||
|
||||
const months = [
|
||||
{ label: "Jan", value: "January" },
|
||||
{ label: "Feb", value: "February" },
|
||||
{ label: "Mar", value: "March" },
|
||||
{ label: "Apr", value: "April" },
|
||||
{ label: "May", value: "May" },
|
||||
{ label: "Jun", value: "June" },
|
||||
{ label: "Jul", value: "July" },
|
||||
{ label: "Aug", value: "August" },
|
||||
{ label: "Sep", value: "September" },
|
||||
{ label: "Oct", value: "October" },
|
||||
{ label: "Nov", value: "November" },
|
||||
{ label: "Dec", value: "December" },
|
||||
];
|
||||
|
||||
const cron_manager = new CronExpressionManager();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Schedule</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Schedule Task</DialogTitle>
|
||||
<div className="max-w-md space-y-6 p-2">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Repeat</Label>
|
||||
|
||||
<Select
|
||||
onValueChange={(value: any) => setFrequency(value)}
|
||||
defaultValue="minute"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minute">Every Minute</SelectItem>
|
||||
<SelectItem value="hour">Every Hour</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{frequency === "hour" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>At minute</Label>
|
||||
<Select
|
||||
value={selectedMinute}
|
||||
onValueChange={setSelectedMinute}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue placeholder="Select minute" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 15, 30, 45].map((min) => (
|
||||
<SelectItem key={min} value={min.toString()}>
|
||||
{min}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency === "custom" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Every</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
className="w-20"
|
||||
value={customInterval.value}
|
||||
onChange={(e) =>
|
||||
setCustomInterval({
|
||||
...customInterval,
|
||||
value: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={customInterval.unit}
|
||||
onValueChange={(value: any) =>
|
||||
setCustomInterval({ ...customInterval, unit: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Minutes</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{frequency === "weekly" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>On</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedDays.length === weekDays.length) {
|
||||
setSelectedDays([]);
|
||||
} else {
|
||||
setSelectedDays(weekDays.map((day) => day.value));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDays.length === weekDays.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedDays([1, 2, 3, 4, 5])}
|
||||
>
|
||||
Weekdays
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedDays([0, 6])}
|
||||
>
|
||||
Weekends
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{weekDays.map((day) => (
|
||||
<Button
|
||||
key={day.value}
|
||||
variant={
|
||||
selectedDays.includes(day.value) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(day.value)
|
||||
? prev.filter((d) => d !== day.value)
|
||||
: [...prev, day.value],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{day.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{frequency === "monthly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Days of Month</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={!showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(false);
|
||||
setSelectedDays(
|
||||
Array.from({ length: 31 }, (_, i) => i + 1),
|
||||
);
|
||||
}}
|
||||
>
|
||||
All Days
|
||||
</Button>
|
||||
<Button
|
||||
variant={showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(true);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
>
|
||||
Customize
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setSelectedDays([15])}>
|
||||
15th
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setSelectedDays([31])}>
|
||||
Last Day
|
||||
</Button>
|
||||
</div>
|
||||
{showCustomDays && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 31 }, (_, i) => (
|
||||
<Button
|
||||
key={i + 1}
|
||||
variant={
|
||||
selectedDays.includes(i + 1) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(i + 1)
|
||||
? prev.filter((d) => d !== i + 1)
|
||||
: [...prev, i + 1],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{frequency === "yearly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Months</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedDays.length === months.length) {
|
||||
setSelectedDays([]);
|
||||
} else {
|
||||
setSelectedDays(Array.from({ length: 12 }, (_, i) => i));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDays.length === months.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{months.map((month, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={
|
||||
selectedDays.includes(index) ? "default" : "outline"
|
||||
}
|
||||
className="px-2 py-1"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(index)
|
||||
? prev.filter((m) => m !== index)
|
||||
: [...prev, index],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{month.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency !== "minute" && frequency !== "hour" && (
|
||||
<div className="flex items-center gap-4 space-y-2">
|
||||
<Label className="pt-2">At</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={selectedTime}
|
||||
onChange={(e) => setSelectedTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
{/*
|
||||
|
||||
On the backend, we are using standard cron expressions,
|
||||
which makes it challenging to add an end date or stop execution
|
||||
after a certain time using only cron expressions.
|
||||
(since standard cron expressions have limitations, like the lack of a year field or more...).
|
||||
|
||||
We could also use ranges in cron expression for end dates but It doesm't cover all cases (sometimes break)
|
||||
|
||||
To automatically end the scheduler, we need to store the end date and time occurrence in the database
|
||||
and modify scheduler.add_job. Currently, we can only stop the scheduler manually from the monitor tab.
|
||||
|
||||
*/}
|
||||
|
||||
{/* <div className="space-y-6">
|
||||
<Label className="text-lg font-medium">Ends</Label>
|
||||
<RadioGroup
|
||||
value={endType}
|
||||
onValueChange={(value: "never" | "on" | "after") =>
|
||||
setEndType(value)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="never" id="never" />
|
||||
<Label htmlFor="never">Never</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="on" id="on" />
|
||||
<Label htmlFor="on" className="w-[50px]">
|
||||
On
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={endType !== "on"}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{endDate ? format(endDate, "PPP") : "Pick a date"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={setEndDate}
|
||||
disabled={(date) => date < new Date()}
|
||||
fromDate={new Date()}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="after" id="after" />
|
||||
<Label htmlFor="after" className="ml-2 w-[50px]">
|
||||
After
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="ml-2 w-[100px]"
|
||||
value={occurrences}
|
||||
onChange={(e) => setOccurrences(Number(e.target.value))}
|
||||
/>
|
||||
<span>times</span>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div> */}
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const cronExpr = cron_manager.generateCronExpression(
|
||||
frequency,
|
||||
selectedTime,
|
||||
selectedDays,
|
||||
selectedMinute,
|
||||
customInterval,
|
||||
);
|
||||
setFrequency("minute");
|
||||
setSelectedDays([]);
|
||||
setSelectedTime("00:00");
|
||||
setShowCustomDays(false);
|
||||
setSelectedMinute("0");
|
||||
setCustomInterval({ value: 1, unit: "minutes" });
|
||||
setOpen(false);
|
||||
afterCronCreation(cronExpr);
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
import { Schedule } from "@/lib/autogpt-server-api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ClockIcon, Loader2 } from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { CronExpressionManager } from "@/lib/monitor/cronExpressionManager";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface SchedulesTableProps {
|
||||
schedules: Schedule[];
|
||||
agents: GraphMeta[];
|
||||
onToggleSchedule: (scheduleId: string, enabled: boolean) => void;
|
||||
sortColumn: keyof Schedule;
|
||||
sortDirection: "asc" | "desc";
|
||||
onSort: (column: keyof Schedule) => void;
|
||||
}
|
||||
|
||||
export const SchedulesTable = ({
|
||||
schedules,
|
||||
agents,
|
||||
onToggleSchedule,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
}: SchedulesTableProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const cron_manager = new CronExpressionManager();
|
||||
const [selectedAgent, setSelectedAgent] = useState<string>("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<string>("");
|
||||
|
||||
const filteredAndSortedSchedules = [...schedules]
|
||||
.filter(
|
||||
(schedule) => !selectedFilter || schedule.graph_id === selectedFilter,
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aValue = a[sortColumn];
|
||||
const bValue = b[sortColumn];
|
||||
if (sortDirection === "asc") {
|
||||
return String(aValue).localeCompare(String(bValue));
|
||||
}
|
||||
return String(bValue).localeCompare(String(aValue));
|
||||
});
|
||||
|
||||
const handleToggleSchedule = (scheduleId: string, enabled: boolean) => {
|
||||
onToggleSchedule(scheduleId, enabled);
|
||||
if (!enabled) {
|
||||
toast({
|
||||
title: "Schedule Disabled",
|
||||
description: "The schedule has been successfully disabled.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewSchedule = () => {
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAgentSelect = (agentId: string) => {
|
||||
setSelectedAgent(agentId);
|
||||
};
|
||||
|
||||
const handleSchedule = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
router.push(`/build?flowID=${selectedAgent}&open_scheduling=true`);
|
||||
} catch (error) {
|
||||
console.error("Navigation error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-fit p-4">
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select Agent for New Schedule</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Select onValueChange={handleAgentSelect}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{agents.map((agent, i) => (
|
||||
<SelectItem key={agent.id + i} value={agent.id}>
|
||||
{agent.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={handleSchedule}
|
||||
disabled={isLoading || !selectedAgent}
|
||||
className="mt-4"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Schedule"
|
||||
)}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Schedules</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select onValueChange={setSelectedFilter}>
|
||||
<SelectTrigger className="h-8 w-[180px] rounded-md px-3 text-xs">
|
||||
<SelectValue placeholder="Filter by graph" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="text-xs">
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" onClick={handleNewSchedule}>
|
||||
<ClockIcon className="mr-2 h-4 w-4" />
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="max-h-[400px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
onClick={() => onSort("graph_id")}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Graph Name
|
||||
</TableHead>
|
||||
<TableHead
|
||||
onClick={() => onSort("id")}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
ID
|
||||
</TableHead>
|
||||
<TableHead
|
||||
onClick={() => onSort("schedule")}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Schedule
|
||||
</TableHead>
|
||||
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredAndSortedSchedules.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="py-8 text-center text-lg text-gray-400"
|
||||
>
|
||||
No schedules are available
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredAndSortedSchedules.map((schedule) => (
|
||||
<TableRow key={schedule.id}>
|
||||
<TableCell className="font-medium">
|
||||
{agents.find((a) => a.id === schedule.graph_id)?.name ||
|
||||
schedule.graph_id}
|
||||
</TableCell>
|
||||
<TableCell>{schedule.id}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{cron_manager.generateDescription(schedule.schedule)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => handleToggleSchedule(schedule.id, false)}
|
||||
>
|
||||
Disable
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
);
|
||||
};
|
@ -29,6 +29,9 @@ interface RunSettingsUiProps {
|
||||
blockInputs: BlockInput[];
|
||||
onInputChange: (nodeId: string, field: string, value: string) => void;
|
||||
onRun: () => void;
|
||||
onSchedule: () => Promise<void>;
|
||||
scheduledInput: boolean;
|
||||
isScheduling: boolean;
|
||||
isRunning: boolean;
|
||||
}
|
||||
|
||||
@ -36,8 +39,11 @@ export function RunnerInputUI({
|
||||
isOpen,
|
||||
onClose,
|
||||
blockInputs,
|
||||
isScheduling,
|
||||
onInputChange,
|
||||
onRun,
|
||||
onSchedule,
|
||||
scheduledInput,
|
||||
isRunning,
|
||||
}: RunSettingsUiProps) {
|
||||
const handleRun = () => {
|
||||
@ -45,11 +51,18 @@ export function RunnerInputUI({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSchedule = async () => {
|
||||
onClose();
|
||||
await onSchedule();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="flex max-h-[80vh] flex-col overflow-hidden sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px]">
|
||||
<DialogHeader className="px-4 py-4">
|
||||
<DialogTitle className="text-2xl">Run Settings</DialogTitle>
|
||||
<DialogTitle className="text-2xl">
|
||||
{scheduledInput ? "Schedule Settings" : "Run Settings"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-sm">
|
||||
Configure settings for running your agent.
|
||||
</DialogDescription>
|
||||
@ -59,11 +72,11 @@ export function RunnerInputUI({
|
||||
</div>
|
||||
<DialogFooter className="px-6 py-4">
|
||||
<Button
|
||||
onClick={handleRun}
|
||||
onClick={scheduledInput ? handleSchedule : handleRun}
|
||||
className="px-8 py-2 text-lg"
|
||||
disabled={isRunning}
|
||||
disabled={scheduledInput ? isScheduling : isRunning}
|
||||
>
|
||||
{isRunning ? "Running..." : "Run"}
|
||||
{scheduledInput ? "Schedule" : isRunning ? "Running..." : "Run"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
43
autogpt_platform/frontend/src/components/ui/radio-group.tsx
Normal file
43
autogpt_platform/frontend/src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DotFilledIcon } from "@radix-ui/react-icons";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-neutral-200 border-neutral-900 text-neutral-900 shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-50 dark:border-neutral-800 dark:text-neutral-50 dark:focus-visible:ring-neutral-300",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<DotFilledIcon className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
@ -19,6 +19,7 @@ import Ajv from "ajv";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { InputItem } from "@/components/RunnerUIWrapper";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
|
||||
const ajv = new Ajv({ strict: false, allErrors: true });
|
||||
@ -34,6 +35,7 @@ export default function useAgentGraph(
|
||||
useSearchParams(),
|
||||
usePathname(),
|
||||
];
|
||||
const [isScheduling, setIsScheduling] = useState(false);
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string>("");
|
||||
const [agentName, setAgentName] = useState<string>("");
|
||||
@ -863,6 +865,50 @@ export default function useAgentGraph(
|
||||
}));
|
||||
}, [saveRunRequest]);
|
||||
|
||||
// runs after saving cron expression and inputs (if exists)
|
||||
const scheduleRunner = useCallback(
|
||||
async (cronExpression: string, inputs: InputItem[]) => {
|
||||
await saveAgent();
|
||||
try {
|
||||
const converted = inputs.reduce(
|
||||
(acc, input) => ({
|
||||
...acc,
|
||||
[input.id]: {
|
||||
type: input.type,
|
||||
inputSchema: input.inputSchema,
|
||||
hardcodedValues: input.hardcodedValues,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
if (flowID) {
|
||||
await api.createSchedule(flowID, {
|
||||
cron: cronExpression,
|
||||
input_data: converted,
|
||||
});
|
||||
toast({
|
||||
title: "Agent scheduling successful",
|
||||
});
|
||||
|
||||
// if scheduling is done from the monitor page, then redirect to monitor page after successful scheduling
|
||||
if (searchParams.get("open_scheduling") === "true") {
|
||||
router.push("/");
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error scheduling agent",
|
||||
description: "Please retry",
|
||||
});
|
||||
}
|
||||
},
|
||||
[api, flowID, saveAgent, toast, router, searchParams],
|
||||
);
|
||||
|
||||
return {
|
||||
agentName,
|
||||
setAgentName,
|
||||
@ -875,9 +921,12 @@ export default function useAgentGraph(
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
requestStopRun,
|
||||
scheduleRunner,
|
||||
isSaving: saveRunRequest.state == "saving",
|
||||
isRunning: saveRunRequest.state == "running",
|
||||
isStopping: saveRunRequest.state == "stopping",
|
||||
isScheduling,
|
||||
setIsScheduling,
|
||||
nodes,
|
||||
setNodes,
|
||||
edges,
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
NodeExecutionResult,
|
||||
OAuth2Credentials,
|
||||
User,
|
||||
ScheduleCreatable,
|
||||
ScheduleUpdateable,
|
||||
} from "./types";
|
||||
|
||||
export default class BaseAutoGPTServerAPI {
|
||||
@ -238,6 +240,31 @@ export default class BaseAutoGPTServerAPI {
|
||||
return this._request("GET", path, query);
|
||||
}
|
||||
|
||||
// Scheduling request
|
||||
async createSchedule(
|
||||
graphId: string,
|
||||
schedule: ScheduleCreatable,
|
||||
): Promise<{ id: string }> {
|
||||
return this._request(
|
||||
"POST",
|
||||
`/graphs/${graphId}/schedules?cron=${schedule.cron}`,
|
||||
{
|
||||
input_data: schedule.input_data,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async updateSchedule(
|
||||
scheduleId: string,
|
||||
update: ScheduleUpdateable,
|
||||
): Promise<{ id: string }> {
|
||||
return this._request("PUT", `/graphs/schedules/${scheduleId}`, update);
|
||||
}
|
||||
|
||||
async getSchedules(graphId: string): Promise<{ [key: string]: string }> {
|
||||
return this._get(`/graphs/${graphId}/schedules`);
|
||||
}
|
||||
|
||||
private async _request(
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
||||
path: string,
|
||||
|
@ -329,3 +329,19 @@ export type AnalyticsDetails = {
|
||||
data: { [key: string]: any };
|
||||
index: string;
|
||||
};
|
||||
|
||||
// Schedule types
|
||||
export type Schedule = {
|
||||
id: string;
|
||||
schedule: string; //cron expression
|
||||
graph_id: string;
|
||||
};
|
||||
|
||||
export type ScheduleCreatable = {
|
||||
cron: string;
|
||||
input_data: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type ScheduleUpdateable = {
|
||||
is_enabled: boolean;
|
||||
};
|
||||
|
@ -0,0 +1,239 @@
|
||||
export class CronExpressionManager {
|
||||
generateCronExpression(
|
||||
frequency: string,
|
||||
selectedTime: string,
|
||||
selectedDays: number[],
|
||||
selectedMinute: string,
|
||||
customInterval: { unit: string; value: number },
|
||||
): string {
|
||||
const [hours, minutes] = selectedTime.split(":").map(Number);
|
||||
let expression = "";
|
||||
|
||||
switch (frequency) {
|
||||
case "minute":
|
||||
expression = "* * * * *";
|
||||
break;
|
||||
case "hour":
|
||||
expression = `${selectedMinute} * * * *`;
|
||||
break;
|
||||
case "daily":
|
||||
expression = `${minutes} ${hours} * * *`;
|
||||
break;
|
||||
case "weekly":
|
||||
const days = selectedDays.join(",");
|
||||
expression = `${minutes} ${hours} * * ${days}`;
|
||||
break;
|
||||
case "monthly":
|
||||
const monthDays = selectedDays.sort((a, b) => a - b).join(",");
|
||||
expression = `${minutes} ${hours} ${monthDays} * *`;
|
||||
break;
|
||||
case "yearly":
|
||||
const monthList = selectedDays
|
||||
.map((d) => d + 1)
|
||||
.sort((a, b) => a - b)
|
||||
.join(",");
|
||||
expression = `${minutes} ${hours} 1 ${monthList} *`;
|
||||
break;
|
||||
case "custom":
|
||||
if (customInterval.unit === "minutes") {
|
||||
expression = `*/${customInterval.value} * * * *`;
|
||||
} else if (customInterval.unit === "hours") {
|
||||
expression = `0 */${customInterval.value} * * *`;
|
||||
} else {
|
||||
expression = `${minutes} ${hours} */${customInterval.value} * *`;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
expression = "";
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
generateDescription(cronExpression: string): string {
|
||||
const parts = cronExpression.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
throw new Error("Invalid cron expression format.");
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Handle every minute
|
||||
if (cronExpression === "* * * * *") {
|
||||
return "Every minute";
|
||||
}
|
||||
|
||||
// Handle minute intervals (e.g., */5 * * * *)
|
||||
if (
|
||||
minute.startsWith("*/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.substring(2);
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle hour intervals (e.g., 30 * * * *)
|
||||
if (
|
||||
hour === "*" &&
|
||||
!minute.includes("/") &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
return `Every hour at minute ${minute}`;
|
||||
}
|
||||
|
||||
// Handle every N hours (e.g., 0 */2 * * *)
|
||||
if (
|
||||
hour.startsWith("*/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.substring(2);
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle daily (e.g., 30 14 * * *)
|
||||
if (
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
return `Every day at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle weekly (e.g., 30 14 * * 1,3,5)
|
||||
if (
|
||||
dayOfWeek !== "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = this.getDayNames(dayOfWeek);
|
||||
return `Every ${days} at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle monthly (e.g., 30 14 1,15 * *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = dayOfMonth.split(",").map(Number);
|
||||
const dayList = days.join(", ");
|
||||
return `On day ${dayList} of every month at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle yearly (e.g., 30 14 1 1,6,12 *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month !== "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const months = this.getMonthNames(month);
|
||||
return `Every year on the 1st day of ${months} at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle custom minute intervals with other fields as * (e.g., every N minutes)
|
||||
if (
|
||||
minute.includes("/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.split("/")[1];
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle custom hour intervals with other fields as * (e.g., every N hours)
|
||||
if (
|
||||
hour.includes("/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.split("/")[1];
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle specific days with custom intervals (e.g., every N days)
|
||||
if (
|
||||
dayOfMonth.startsWith("*/") &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const interval = dayOfMonth.substring(2);
|
||||
return `Every ${interval} days at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
return `Cron Expression: ${cronExpression}`;
|
||||
}
|
||||
|
||||
private formatTime(hour: string, minute: string): string {
|
||||
const formattedHour = this.padZero(hour);
|
||||
const formattedMinute = this.padZero(minute);
|
||||
return `${formattedHour}:${formattedMinute}`;
|
||||
}
|
||||
|
||||
private padZero(value: string): string {
|
||||
return value.padStart(2, "0");
|
||||
}
|
||||
|
||||
private getDayNames(dayOfWeek: string): string {
|
||||
const days = dayOfWeek.split(",").map(Number);
|
||||
const dayNames = days
|
||||
.map((d) => {
|
||||
const names = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
return names[d] || `Unknown(${d})`;
|
||||
})
|
||||
.join(", ");
|
||||
return dayNames;
|
||||
}
|
||||
|
||||
private getMonthNames(month: string): string {
|
||||
const months = month.split(",").map(Number);
|
||||
const monthNames = months
|
||||
.map((m) => {
|
||||
const names = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
return names[m - 1] || `Unknown(${m})`;
|
||||
})
|
||||
.join(", ");
|
||||
return monthNames;
|
||||
}
|
||||
}
|
@ -2326,6 +2326,22 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.1.0"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.2.1.tgz#42b914c85f3a77be3ab766b6e49a9598680f76d1"
|
||||
integrity sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-roving-focus" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user