Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform) : scheduling agent runner #8634

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/data/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def from_db(schedule: AgentGraphExecutionSchedule):

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]
Expand Down
1 change: 1 addition & 0 deletions autogpt_platform/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 59 additions & 1 deletion autogpt_platform/frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you did this due to non-optimized backend, I've improved the API here so you can do GET /schedules and get all the schedules for a user without firing multiple requests and add more information on the GET response:
#8649

I'm happy with any of the ordering, we can first get this merged and improve it and update the routing on my PR, or you can rebase your PR based on the above PR.

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);
Expand All @@ -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
Expand Down Expand Up @@ -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>
);
};
Expand Down
40 changes: 40 additions & 0 deletions autogpt_platform/frontend/src/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,7 +98,10 @@ const FlowEditor: React.FC<{
requestSave,
requestSaveAndRun,
requestStopRun,
scheduleRunner,
isRunning,
isScheduling,
setIsScheduling,
nodes,
setNodes,
edges,
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
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;
}

const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
onClickAgentOutputs,
onClickRunAgent,
onClickScheduleButton,
isRunning,
isDisabled,
isScheduling,
requestStopRun,
runAgentTooltip,
}) => {
Expand Down Expand Up @@ -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>
);
Expand Down
60 changes: 58 additions & 2 deletions autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand All @@ -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
Expand Down
Loading
Loading