Allow running of agents from the monitor page

This commit is contained in:
SwiftyOS
2024-12-11 15:27:58 +01:00
parent ac03211c59
commit b1468f779c
9 changed files with 347 additions and 226 deletions

View File

@@ -1,7 +1,3 @@
export default function HealthPage() {
return (
<div>
Yay im healthy
</div>
);
return <div>Yay im healthy</div>;
}

View File

@@ -98,8 +98,8 @@ export const AgentImportForm: React.FC<
is_active: !values.importAsTemplate,
};
(api.createGraph(payload)
)
api
.createGraph(payload)
.then((response) => {
const qID = "flowID";
window.location.href = `/build?${qID}=${response.id}`;

View File

@@ -55,7 +55,6 @@ export const SaveControl = ({
onSave();
}, [onSave]);
const { toast } = useToast();
useEffect(() => {

View File

@@ -1,6 +1,6 @@
"use client";
import Link from "next/link";
import { ArrowLeft, Download } from "lucide-react";
import { ArrowLeft, Download, Calendar, Tag } from "lucide-react";
import { Button } from "@/components/ui/button";
import AutoGPTServerAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
import "@xyflow/react/dist/style.css";

View File

@@ -1,8 +1,10 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import AutoGPTServerAPI, {
Graph,
GraphMeta,
safeCopyGraph,
BlockUIType,
BlockIORootSchema,
} from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -18,9 +20,9 @@ import {
import { Button, buttonVariants } from "@/components/ui/button";
import { ClockIcon, ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
import Link from "next/link";
import { exportAsJSONFile } from "@/lib/utils";
import { exportAsJSONFile, filterBlocksByType } from "@/lib/utils";
import { FlowRunsStats } from "@/components/monitor/index";
import { Trash2Icon } from "lucide-react";
import { Trash2Icon, Timer } from "lucide-react";
import {
Dialog,
DialogContent,
@@ -29,6 +31,10 @@ import {
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { useToast } from "@/components/ui/use-toast";
import { CronScheduler } from "@/components/cronScheduler";
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
import useAgentGraph from "@/hooks/useAgentGraph";
export const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
@@ -38,7 +44,30 @@ export const FlowInfo: React.FC<
refresh: () => void;
}
> = ({ flow, flowRuns, flowVersion, refresh, ...props }) => {
const {
agentName,
setAgentName,
agentDescription,
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,
requestStopRun,
scheduleRunner,
isRunning,
isScheduling,
setIsScheduling,
nodes,
setNodes,
edges,
setEdges,
} = useAgentGraph(flow.id, false);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const { toast } = useToast();
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(null);
const [selectedVersion, setSelectedFlowVersion] = useState(
@@ -50,11 +79,103 @@ export const FlowInfo: React.FC<
);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [openCron, setOpenCron] = useState(false);
const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false);
const isDisabled = !selectedFlowVersion;
const getBlockInputsAndOutputs = useCallback(() => {
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 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 || [],
limit_to_placeholder_values:
(node.data.hardcodedValues as any).limit_to_placeholder_values ||
false,
},
}));
const outputs = outputBlocks.map((node) => ({
id: node.id,
type: "output" as const,
hardcodedValues: {
name: (node.data.hardcodedValues as any).name || "Output",
description:
(node.data.hardcodedValues as any).description ||
"Output from the agent",
value: (node.data.hardcodedValues as any).value,
},
result: (node.data.executionResults as any)?.at(-1)?.data?.output,
}));
return { inputs, outputs };
}, [nodes]);
const handleScheduleButton = () => {
if (!selectedFlowVersion) {
toast({
title: "Please select a flow version before scheduling",
duration: 2000,
});
return;
}
setOpenCron(true);
};
useEffect(() => {
api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result));
}, [flow.id, api]);
const openRunnerInput = () => setIsRunnerInputOpen(true);
const runOrOpenInput = () => {
const { inputs } = getBlockInputsAndOutputs();
if (inputs.length > 0) {
openRunnerInput();
} else {
requestSaveAndRun();
}
};
const handleInputChange = useCallback(
(nodeId: string, field: string, value: string) => {
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],
);
return (
<Card {...props}>
<CardHeader className="flex-row justify-between space-x-3 space-y-0">
@@ -62,9 +183,6 @@ export const FlowInfo: React.FC<
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
</CardTitle>
<p className="mt-2">
Agent ID: <code>{flow.id}</code>
</p>
</div>
<div className="flex items-start space-x-2">
{(flowVersions?.length ?? 0) > 1 && (
@@ -130,6 +248,15 @@ export const FlowInfo: React.FC<
>
<ExitIcon className="mr-2" /> Export
</Button>
<Button
variant="secondary"
className="bg-purple-500 text-white hover:bg-purple-700"
onClick={isRunning ? requestStopRun : runOrOpenInput}
disabled={isDisabled}
title={!isRunning ? "Run Agent" : "Stop Agent"}
>
{isRunning ? "Stop Agent" : "Run Agent"}
</Button>
<Button
variant="outline"
onClick={() => setIsDeleteModalOpen(true)}
@@ -179,6 +306,20 @@ export const FlowInfo: React.FC<
</DialogFooter>
</DialogContent>
</Dialog>
<RunnerInputUI
isOpen={isRunnerInputOpen}
onClose={() => setIsRunnerInputOpen(false)}
blockInputs={getBlockInputsAndOutputs().inputs}
onInputChange={handleInputChange}
onRun={() => {
setIsRunnerInputOpen(false);
requestSaveAndRun();
}}
isRunning={isRunning}
scheduledInput={false}
isScheduling={false}
onSchedule={async () => {}} // Fixed type error by making async
/>
</Card>
);
};

View File

@@ -97,12 +97,7 @@ export const FlowRunInfo: React.FC<
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
</CardTitle>
<p className="mt-2">
Agent ID: <code>{flow.id}</code>
</p>
<p className="mt-1">
Run ID: <code>{flowRun.id}</code>
</p>
</div>
<div className="flex space-x-2">
{flowRun.status === "running" && (
@@ -122,6 +117,12 @@ export const FlowRunInfo: React.FC<
</div>
</CardHeader>
<CardContent>
<p className="hidden">
<strong>Agent ID:</strong> <code>{flow.id}</code>
</p>
<p className="hidden">
<strong>Run ID:</strong> <code>{flowRun.id}</code>
</p>
<div>
<strong>Status:</strong>{" "}
<FlowRunStatusBadge status={flowRun.status} />
@@ -138,6 +139,7 @@ export const FlowRunInfo: React.FC<
<strong>Duration (run time):</strong> {flowRun.duration} (
{flowRun.totalRunTime}) seconds
</p>
</CardContent>
</Card>
<RunnerOutputUI

View File

@@ -24,7 +24,10 @@ import { GraphMeta } from "@/lib/autogpt-server-api";
const ajv = new Ajv({ strict: false, allErrors: true });
export default function useAgentGraph(flowID?: string, passDataToBeads?: boolean) {
export default function useAgentGraph(
flowID?: string,
passDataToBeads?: boolean,
) {
const { toast } = useToast();
const [router, searchParams, pathname] = [
useRouter(),
@@ -637,200 +640,189 @@ export default function useAgentGraph(flowID?: string, passDataToBeads?: boolean
[availableNodes],
);
const _saveAgent = useCallback(
async () => {
//FIXME frontend ids should be resolved better (e.g. returned from the server)
// currently this relays on block_id and position
const blockIdToNodeIdMap: Record<string, string> = {};
const _saveAgent = useCallback(async () => {
//FIXME frontend ids should be resolved better (e.g. returned from the server)
// currently this relays on block_id and position
const blockIdToNodeIdMap: Record<string, string> = {};
nodes.forEach((node) => {
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
nodes.forEach((node) => {
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
const formattedNodes = nodes.map((node) => {
const inputDefault = prepareNodeInputData(node);
const inputNodes = edges
.filter((edge) => edge.target === node.id)
.map((edge) => ({
name: edge.targetHandle || "",
node_id: edge.source,
}));
const outputNodes = edges
.filter((edge) => edge.source === node.id)
.map((edge) => ({
name: edge.sourceHandle || "",
node_id: edge.target,
}));
return {
id: node.id,
block_id: node.data.block_id,
input_default: inputDefault,
input_nodes: inputNodes,
output_nodes: outputNodes,
data: {
...node.data,
hardcodedValues: removeEmptyStringsAndNulls(
node.data.hardcodedValues,
),
},
metadata: { position: node.position },
};
});
const links = edges.map((edge) => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || "",
sink_name: edge.targetHandle || "",
}));
const payload = {
id: savedAgent?.id!,
name: agentName || `New Agent ${new Date().toISOString()}`,
description: agentDescription || "",
nodes: formattedNodes,
links: links,
};
// To avoid saving the same graph, we compare the payload with the saved agent.
// Differences in IDs are ignored.
const comparedPayload = {
...(({ id, ...rest }) => rest)(payload),
nodes: payload.nodes.map(
({ id, data, input_nodes, output_nodes, ...rest }) => rest,
),
links: payload.links.map(({ source_id, sink_id, ...rest }) => rest),
};
const comparedSavedAgent = {
name: savedAgent?.name,
description: savedAgent?.description,
nodes: savedAgent?.nodes.map((v) => ({
block_id: v.block_id,
input_default: v.input_default,
metadata: v.metadata,
})),
links: savedAgent?.links.map((v) => ({
sink_name: v.sink_name,
source_name: v.source_name,
})),
};
let newSavedAgent = null;
if (savedAgent && deepEquals(comparedPayload, comparedSavedAgent)) {
console.warn("No need to save: Graph is the same as version on server");
newSavedAgent = savedAgent;
} else {
console.debug(
"Saving new Graph version; old vs new:",
comparedPayload,
payload,
);
setNodesSyncedWithSavedAgent(false);
newSavedAgent = savedAgent
? await api.updateGraph(savedAgent.id, payload)
: await api.createGraph(payload);
console.debug("Response from the API:", newSavedAgent);
}
// Route the URL to the new flow ID if it's a new agent.
if (!savedAgent) {
const path = new URLSearchParams(searchParams);
path.set("flowID", newSavedAgent.id);
router.push(`${pathname}?${path.toString()}`);
return;
}
// Update the node IDs on the frontend
setSavedAgent(newSavedAgent);
setNodes((prev) => {
return newSavedAgent.nodes
.map((backendNode) => {
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
const frontendNodeId = blockIdToNodeIdMap[key];
const frontendNode = prev.find(
(node) => node.id === frontendNodeId,
);
return frontendNode
? {
...frontendNode,
position: backendNode.metadata.position,
data: {
...frontendNode.data,
hardcodedValues: removeEmptyStringsAndNulls(
frontendNode.data.hardcodedValues,
),
status: undefined,
backend_id: backendNode.id,
webhookId: backendNode.webhook_id,
executionResults: [],
},
}
: null;
})
.filter((node) => node !== null);
});
// Reset bead count
setEdges((edges) => {
return edges.map((edge) => ({
...edge,
data: {
...edge.data,
edgeColor: edge.data?.edgeColor!,
beadUp: 0,
beadDown: 0,
beadData: [],
},
const formattedNodes = nodes.map((node) => {
const inputDefault = prepareNodeInputData(node);
const inputNodes = edges
.filter((edge) => edge.target === node.id)
.map((edge) => ({
name: edge.targetHandle || "",
node_id: edge.source,
}));
});
},
[
api,
nodes,
edges,
pathname,
router,
searchParams,
savedAgent,
agentName,
agentDescription,
prepareNodeInputData,
],
);
const saveAgent = useCallback(
async () => {
try {
await _saveAgent();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("Error saving agent", error);
toast({
variant: "destructive",
title: "Error saving agent",
description: errorMessage,
});
}
},
[_saveAgent, toast],
);
const outputNodes = edges
.filter((edge) => edge.source === node.id)
.map((edge) => ({
name: edge.sourceHandle || "",
node_id: edge.target,
}));
const requestSave = useCallback(
() => {
saveAgent();
setSaveRunRequest({
request: "save",
state: "saving",
return {
id: node.id,
block_id: node.data.block_id,
input_default: inputDefault,
input_nodes: inputNodes,
output_nodes: outputNodes,
data: {
...node.data,
hardcodedValues: removeEmptyStringsAndNulls(
node.data.hardcodedValues,
),
},
metadata: { position: node.position },
};
});
const links = edges.map((edge) => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || "",
sink_name: edge.targetHandle || "",
}));
const payload = {
id: savedAgent?.id!,
name: agentName || `New Agent ${new Date().toISOString()}`,
description: agentDescription || "",
nodes: formattedNodes,
links: links,
};
// To avoid saving the same graph, we compare the payload with the saved agent.
// Differences in IDs are ignored.
const comparedPayload = {
...(({ id, ...rest }) => rest)(payload),
nodes: payload.nodes.map(
({ id, data, input_nodes, output_nodes, ...rest }) => rest,
),
links: payload.links.map(({ source_id, sink_id, ...rest }) => rest),
};
const comparedSavedAgent = {
name: savedAgent?.name,
description: savedAgent?.description,
nodes: savedAgent?.nodes.map((v) => ({
block_id: v.block_id,
input_default: v.input_default,
metadata: v.metadata,
})),
links: savedAgent?.links.map((v) => ({
sink_name: v.sink_name,
source_name: v.source_name,
})),
};
let newSavedAgent = null;
if (savedAgent && deepEquals(comparedPayload, comparedSavedAgent)) {
console.warn("No need to save: Graph is the same as version on server");
newSavedAgent = savedAgent;
} else {
console.debug(
"Saving new Graph version; old vs new:",
comparedPayload,
payload,
);
setNodesSyncedWithSavedAgent(false);
newSavedAgent = savedAgent
? await api.updateGraph(savedAgent.id, payload)
: await api.createGraph(payload);
console.debug("Response from the API:", newSavedAgent);
}
// Route the URL to the new flow ID if it's a new agent.
if (!savedAgent) {
const path = new URLSearchParams(searchParams);
path.set("flowID", newSavedAgent.id);
router.push(`${pathname}?${path.toString()}`);
return;
}
// Update the node IDs on the frontend
setSavedAgent(newSavedAgent);
setNodes((prev) => {
return newSavedAgent.nodes
.map((backendNode) => {
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
const frontendNodeId = blockIdToNodeIdMap[key];
const frontendNode = prev.find((node) => node.id === frontendNodeId);
return frontendNode
? {
...frontendNode,
position: backendNode.metadata.position,
data: {
...frontendNode.data,
hardcodedValues: removeEmptyStringsAndNulls(
frontendNode.data.hardcodedValues,
),
status: undefined,
backend_id: backendNode.id,
webhookId: backendNode.webhook_id,
executionResults: [],
},
}
: null;
})
.filter((node) => node !== null);
});
// Reset bead count
setEdges((edges) => {
return edges.map((edge) => ({
...edge,
data: {
...edge.data,
edgeColor: edge.data?.edgeColor!,
beadUp: 0,
beadDown: 0,
beadData: [],
},
}));
});
}, [
api,
nodes,
edges,
pathname,
router,
searchParams,
savedAgent,
agentName,
agentDescription,
prepareNodeInputData,
]);
const saveAgent = useCallback(async () => {
try {
await _saveAgent();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("Error saving agent", error);
toast({
variant: "destructive",
title: "Error saving agent",
description: errorMessage,
});
},
[saveAgent],
);
}
}, [_saveAgent, toast]);
const requestSave = useCallback(() => {
saveAgent();
setSaveRunRequest({
request: "save",
state: "saving",
});
}, [saveAgent]);
const requestSaveAndRun = useCallback(() => {
saveAgent();

View File

@@ -104,23 +104,18 @@ export default class BaseAutoGPTServerAPI {
return this._get(`/graphs/${id}`, query);
}
getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
createGraph(
graphID: GraphCreatable | string,
): Promise<Graph> {
createGraph(graphID: GraphCreatable | string): Promise<Graph> {
let requestBody = { graph: graphID } as GraphCreateRequestBody;
return this._request("POST", "/graphs", requestBody);
}
updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return this._request("PUT", `/graphs/${id}`, graph);
}
@@ -615,11 +610,11 @@ export default class BaseAutoGPTServerAPI {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}

View File

@@ -221,11 +221,7 @@ export type Graph = GraphMeta & {
export type GraphUpdateable = Omit<
Graph,
| "version"
| "is_active"
| "links"
| "input_schema"
| "output_schema"
"version" | "is_active" | "links" | "input_schema" | "output_schema"
> & {
version?: number;
is_active?: boolean;