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): Agent Library v2 / Runs page #9051

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions autogpt_platform/backend/backend/data/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ def has_value(name):

# Validate dependencies between fields
for field_name, field_info in input_schema.items():

# Apply input dependency validation only on run & field with depends_on
json_schema_extra = field_info.json_schema_extra or {}
dependencies = json_schema_extra.get("depends_on", [])
Expand Down Expand Up @@ -475,14 +474,22 @@ async def get_graphs(
return graph_models


async def get_executions(user_id: str) -> list[GraphExecution]:
async def get_graphs_executions(user_id: str) -> list[GraphExecution]:
executions = await AgentGraphExecution.prisma().find_many(
where={"userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecution.from_db(execution) for execution in executions]


async def get_graph_executions(graph_id: str, user_id: str) -> list[GraphExecution]:
executions = await AgentGraphExecution.prisma().find_many(
where={"agentGraphId": graph_id, "userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecution.from_db(execution) for execution in executions]


async def get_execution(user_id: str, execution_id: str) -> GraphExecution | None:
execution = await AgentGraphExecution.prisma().find_first(
where={"id": execution_id, "userId": user_id}
Expand Down
16 changes: 14 additions & 2 deletions autogpt_platform/backend/backend/server/routers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,22 @@ async def stop_graph_run(
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_executions(
async def get_graphs_executions(
user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecution]:
return await graph_db.get_executions(user_id=user_id)
return await graph_db.get_graphs_executions(user_id=user_id)


@v1_router.get(
path="/graphs/{graph_id}/executions",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graph_executions(
graph_id: str,
user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecution]:
return await graph_db.get_graph_executions(graph_id=graph_id, user_id=user_id)


@v1_router.get(
Expand Down
2 changes: 2 additions & 0 deletions autogpt_platform/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@sentry/nextjs": "^8",
Expand Down
316 changes: 316 additions & 0 deletions autogpt_platform/frontend/src/app/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Plus } from "lucide-react";

import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
Schedule,
GraphMeta,
BlockIOSubType,
} from "@/lib/autogpt-server-api";

import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

import { Button } from "@/components/agptui/Button";
import { AgentRunStatus } from "@/components/agptui/AgentRunStatusChip";
import AgentRunSummaryCard from "@/components/agptui/AgentRunSummaryCard";
import moment from "moment";

const agentRunStatusMap: Record<GraphExecution["status"], AgentRunStatus> = {
COMPLETED: "success",
FAILED: "failed",
QUEUED: "queued",
RUNNING: "running",
// TODO: implement "draft"
// TODO: implement "stopped"
};

export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: string } = useParams();
const router = useRouter();
const api = useBackendAPI();

const [agent, setAgent] = useState<GraphMeta | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecution[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedRun, setSelectedRun] = useState<
GraphExecution | Schedule | null
>(null);
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
"runs",
);

const fetchAgents = useCallback(() => {
api.getGraph(agentID).then(setAgent);
api.getGraphExecutions(agentID).then((agentRuns) => {
setAgentRuns(agentRuns.toSorted((a, b) => b.started_at - a.started_at));

if (!selectedRun) {
setSelectedRun(agentRuns[0]);
}
});
}, [api, agentID, selectedRun]);

useEffect(() => {
fetchAgents();
}, [fetchAgents]);

const fetchSchedules = useCallback(async () => {
// TODO: filter in backend
setSchedules(
(await api.listSchedules()).filter((s) => s.graph_id == agentID),
);
}, [api, agentID]);

useEffect(() => {
fetchSchedules();
}, [fetchSchedules]);

const removeSchedule = useCallback(
async (scheduleId: string) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);

/* TODO: use websockets instead of polling */
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents, agent]);

const selectedRunStatus: AgentRunStatus = useMemo(
() =>
!selectedRun
? "draft"
: "status" in selectedRun
? agentRunStatusMap[selectedRun.status]
: "scheduled",
[selectedRun],
);

const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
if (!selectedRun) return [];
return [
{
label: "Status",
value:
selectedRunStatus.charAt(0).toUpperCase() +
selectedRunStatus.slice(1),
},
...("started_at" in selectedRun
? [
{
label: "Started",
value: `${moment(selectedRun.started_at).fromNow()}, ${moment(selectedRun.started_at).format("HH:mm")}`,
},
{
label: "Duration",
value: `${moment.duration(selectedRun.duration, "seconds").humanize()}`,
},
// { label: "Cost", value: selectedRun.cost }, // TODO: implement cost
]
: [{ label: "Scheduled for", value: selectedRun.next_run_time }]),
];
}, [selectedRun, selectedRunStatus]);

const agentRunInputs: Record<string, { type: BlockIOSubType; value: any }> =
useMemo(() => {
if (!selectedRun) return {};
// return selectedRun.input; // TODO: implement run input view
return {
"Mock Input": { type: "string", value: "Mock Value" },
};
}, [selectedRun]);

const runAgain = useCallback(
() =>
api.executeGraph(
agentID,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
),
[api, agentID, agentRunInputs],
);

const agentRunOutputs: Record<string, { type: BlockIOSubType; value: any }> =
useMemo(() => {
if (
!selectedRun ||
!["running", "success", "failed"].includes(selectedRunStatus) ||
!("output" in selectedRun)
)
return {};
// return selectedRun.output; // TODO: implement run output view
return {
"Mock Output": { type: "string", value: "Mock Value" },
};
}, [selectedRun, selectedRunStatus]);

const runActions: { label: string; callback: () => void }[] = useMemo(() => {
if (!selectedRun) return [];
return [{ label: "Run again", callback: () => runAgain() }];
}, [selectedRun, runAgain]);

const agentActions: { label: string; callback: () => void }[] =
useMemo(() => {
if (!agentID) return [];
return [
{
label: "Open in builder",
callback: () => router.push(`/build?flowID=${agentID}`),
},
];
}, [agentID, router]);

if (!agent) {
/* TODO: implement loading indicators / skeleton page */
return <span>Loading...</span>;
}

return (
<div className="flex gap-8 container">
<aside className="flex w-72 flex-col gap-4">
<Button className="flex w-full items-center gap-2 py-6">
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>

<div className="flex gap-2">
<Badge
variant={activeListTab === "runs" ? "secondary" : "outline"}
className="cursor-pointer gap-2"
onClick={() => setActiveListTab("runs")}
>
<span>Runs</span>
<span className="text-neutral-600">{agentRuns.length}</span>
</Badge>

<Badge
variant={activeListTab === "scheduled" ? "secondary" : "outline"}
className="cursor-pointer gap-2"
onClick={() => setActiveListTab("scheduled")}
>
<span>Scheduled</span>
<span className="text-neutral-600">
{schedules.filter((s) => s.graph_id === agentID).length}
</span>
</Badge>
</div>

<ScrollArea className="h-[calc(100vh-200px)]">
<div className="flex flex-col gap-2">
{activeListTab === "runs"
? agentRuns.map((run, i) => (
<AgentRunSummaryCard
key={i}
agentID={run.graph_id}
agentRunID={run.execution_id}
status={agentRunStatusMap[run.status]}
title={agent.name}
timestamp={run.started_at}
onClick={() => setSelectedRun(run)}
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agentID)
.map((schedule, i) => (
<AgentRunSummaryCard
key={i}
agentID={schedule.graph_id}
agentRunID={schedule.id}
status="scheduled"
title={schedule.name}
timestamp={schedule.next_run_time} // FIXME
onClick={() => setSelectedRun(schedule)}
/>
))}
</div>
</ScrollArea>
</aside>

<div className="flex-1">
<h1 className="mb-8 text-3xl font-medium">
{agent.name /* TODO: use dynamic/custom run title */}
</h1>

<Tabs defaultValue="info">
<TabsList>
<TabsTrigger value="info">Info</TabsTrigger>
<TabsTrigger value="output">Output</TabsTrigger>
<TabsTrigger value="input">Input</TabsTrigger>
<TabsTrigger value="rate">Rate</TabsTrigger>
</TabsList>

<TabsContent value="info">
<Card>
<CardHeader>
<CardTitle>Info</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-stretch gap-4">
{infoStats.map(({ label, value }) => (
<div key={label} className="flex-1">
<p className="text-sm font-medium text-black">{label}</p>
<p className="text-sm text-neutral-600">{value}</p>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>

<TabsContent value="input">
<Card>
<CardHeader>
<CardTitle>Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{Object.entries(agentRunInputs).map(([key, { value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
<Input
defaultValue={value}
className="rounded-full"
disabled
/>
</div>
))}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>

<aside className="w-64">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>

<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}
2 changes: 1 addition & 1 deletion autogpt_platform/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default async function RootLayout({
},
]}
/>
<main className="flex-1 p-4">{children}</main>
<main className="flex-1 p-4 w-full">{children}</main>
<TallyPopupSimple />
</div>
<Toaster />
Expand Down
Loading
Loading