Skip to content

Commit

Permalink
crude first draft of Agent Runs page (EOD)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pwuts committed Dec 19, 2024
1 parent abc5357 commit 76b9180
Show file tree
Hide file tree
Showing 11 changed files with 577 additions and 20 deletions.
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
307 changes: 307 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,307 @@
"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">
<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-evenly gap-4">
{infoStats.map(({label, value}) => (
<div key={label}>
<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>
);
}
Loading

0 comments on commit 76b9180

Please sign in to comment.