Merge branch 'master' into aarushikansal/open-1426-server-setup-user-auth-add-user-table

This commit is contained in:
Aarushi
2024-08-07 13:56:10 +01:00
committed by GitHub
57 changed files with 2992 additions and 1372 deletions

2
.gitattributes vendored
View File

@@ -6,3 +6,5 @@ docs/_javascript/** linguist-vendored
# Exclude VCR cassettes from stats
forge/tests/vcr_cassettes/**/**.y*ml linguist-generated
* text=auto

View File

@@ -39,8 +39,8 @@ jobs:
if: matrix.db-platform == 'postgres'
uses: ikalnytskyi/action-setup-postgres@v6
with:
username: ${{ secrets.DB_USER }}
password: ${{ secrets.DB_PASS }}
username: ${{ secrets.DB_USER || 'postgres' }}
password: ${{ secrets.DB_PASS || 'postgres' }}
database: postgres
port: 5432
id: postgres
@@ -145,13 +145,13 @@ jobs:
CI: true
PLAIN_OUTPUT: True
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_USER: ${{ secrets.DB_USER || 'postgres' }}
DB_PASS: ${{ secrets.DB_PASS || 'postgres' }}
DB_NAME: postgres
DB_PORT: 5432
RUN_ENV: local
PORT: 8080
DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5432/${{ secrets.DB_NAME }}
DATABASE_URL: postgresql://${{ secrets.DB_USER || 'postgres' }}:${{ secrets.DB_PASS || 'postgres' }}@localhost:5432/${{ secrets.DB_NAME || 'postgres'}}
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v4

View File

@@ -215,4 +215,10 @@ If you would like to implement one of these blocks, open a pull request and we w
- Read / Get most read books in a given month, year, etc from GoodReads or Amazon Books, etc
- Get dates for specific shows across all streaming services
- Suggest/Recommend/Get most watched shows in a given month, year, etc across all streaming platforms
- Data analysis from xlsx data set
- Gather via Excel or Google Sheets data > Sample the data randomly (sample block takes top X, bottom X, randomly, etc) > pass that to LLM Block to generate a script for analysis of the full data > Python block to run the script> making a loop back through LLM Fix Block on error > create chart/visualization (potentially in the code block?) > show the image as output (this may require frontend changes to show)
- Tiktok video search and download
### Marketing
- Portfolio site design and enhancements

View File

@@ -18,6 +18,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",

View File

@@ -6,6 +6,7 @@ import { NavBar } from "@/components/NavBar";
import { cn } from "@/lib/utils";
import "./globals.css";
import TallyPopupSimple from "@/components/TallyPopup";
const inter = Inter({ subsets: ["latin"] });
@@ -32,6 +33,7 @@ export default function RootLayout({
<div className="flex flex-col min-h-screen ">
<NavBar />
<main className="flex-1 p-4 overflow-hidden">{children}</main>
<TallyPopupSimple />
</div>
</Providers>
</body>

View File

@@ -1,66 +1,20 @@
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import moment from "moment";
import {
ComposedChart,
DefaultLegendContentProps,
Legend,
Line,
ResponsiveContainer,
Scatter,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import AutoGPTServerAPI, {
Graph,
GraphMeta,
NodeExecutionResult,
safeCopyGraph,
} from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
import { FlowRun } from "@/lib/types";
import {
ChevronDownIcon,
ClockIcon,
EnterIcon,
ExitIcon,
Pencil2Icon,
} from "@radix-ui/react-icons";
import { cn, exportAsJSONFile, hashString } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button, buttonVariants } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { AgentImportForm } from "@/components/agent-import-form";
AgentFlowList,
FlowInfo,
FlowRunInfo,
FlowRunsList,
FlowRunsStats,
} from "@/components/monitor";
const Monitor = () => {
const [flows, setFlows] = useState<GraphMeta[]>([]);
@@ -165,19 +119,6 @@ const Monitor = () => {
);
};
type FlowRun = {
id: string;
graphID: string;
graphVersion: number;
status: "running" | "waiting" | "success" | "failed";
startTime: number; // unix timestamp (ms)
endTime: number; // unix timestamp (ms)
duration: number; // seconds
totalRunTime: number; // seconds
nodeExecutionResults: NodeExecutionResult[];
};
function flowRunFromNodeExecutionResults(
nodeExecutionResults: NodeExecutionResult[],
): FlowRun {
@@ -230,664 +171,4 @@ function flowRunFromNodeExecutionResults(
};
}
const AgentFlowList = ({
flows,
flowRuns,
selectedFlow,
onSelectFlow,
className,
}: {
flows: GraphMeta[];
flowRuns?: FlowRun[];
selectedFlow: GraphMeta | null;
onSelectFlow: (f: GraphMeta) => void;
className?: string;
}) => {
const [templates, setTemplates] = useState<GraphMeta[]>([]);
const api = new AutoGPTServerAPI();
useEffect(() => {
api.listTemplates().then((templates) => setTemplates(templates));
}, []);
return (
<Card className={className}>
<CardHeader className="flex-row justify-between items-center space-x-3 space-y-0">
<CardTitle>Agents</CardTitle>
<div className="flex items-center">
{/* Split "Create" button */}
<Button variant="outline" className="rounded-r-none" asChild>
<Link href="/build">Create</Link>
</Button>
<Dialog>
{/* https://ui.shadcn.com/docs/components/dialog#notes */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className={"rounded-l-none border-l-0 px-2"}
>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DialogTrigger asChild>
<DropdownMenuItem>
<EnterIcon className="mr-2" /> Import from file
</DropdownMenuItem>
</DialogTrigger>
{templates.length > 0 && (
<>
{/* List of templates */}
<DropdownMenuSeparator />
<DropdownMenuLabel>Use a template</DropdownMenuLabel>
{templates.map((template) => (
<DropdownMenuItem
key={template.id}
onClick={() => {
api
.createGraph(template.id, template.version)
.then((newGraph) => {
window.location.href = `/build?flowID=${newGraph.id}`;
});
}}
>
{template.name}
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader className="text-lg">
Import an Agent (template) from a file
</DialogHeader>
<AgentImportForm />
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
{/* <TableHead>Status</TableHead> */}
{/* <TableHead>Last updated</TableHead> */}
{flowRuns && (
<TableHead className="md:hidden lg:table-cell">
# of runs
</TableHead>
)}
{flowRuns && <TableHead>Last run</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{flows
.map((flow) => {
let runCount = 0,
lastRun: FlowRun | null = null;
if (flowRuns) {
const _flowRuns = flowRuns.filter(
(r) => r.graphID == flow.id,
);
runCount = _flowRuns.length;
lastRun =
runCount == 0
? null
: _flowRuns.reduce((a, c) =>
a.startTime > c.startTime ? a : c,
);
}
return { flow, runCount, lastRun };
})
.sort((a, b) => {
if (!a.lastRun && !b.lastRun) return 0;
if (!a.lastRun) return 1;
if (!b.lastRun) return -1;
return b.lastRun.startTime - a.lastRun.startTime;
})
.map(({ flow, runCount, lastRun }) => (
<TableRow
key={flow.id}
className="cursor-pointer"
onClick={() => onSelectFlow(flow)}
data-state={selectedFlow?.id == flow.id ? "selected" : null}
>
<TableCell>{flow.name}</TableCell>
{/* <TableCell><FlowStatusBadge status={flow.status ?? "active"} /></TableCell> */}
{/* <TableCell>
{flow.updatedAt ?? "???"}
</TableCell> */}
{flowRuns && (
<TableCell className="md:hidden lg:table-cell">
{runCount}
</TableCell>
)}
{flowRuns &&
(!lastRun ? (
<TableCell />
) : (
<TableCell title={moment(lastRun.startTime).toString()}>
{moment(lastRun.startTime).fromNow()}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
const FlowStatusBadge = ({
status,
}: {
status: "active" | "disabled" | "failing";
}) => (
<Badge
variant="default"
className={
status === "active"
? "bg-green-500 dark:bg-green-600"
: status === "failing"
? "bg-red-500 dark:bg-red-700"
: "bg-gray-500 dark:bg-gray-600"
}
>
{status}
</Badge>
);
const FlowRunsList: React.FC<{
flows: GraphMeta[];
runs: FlowRun[];
className?: string;
selectedRun?: FlowRun | null;
onSelectRun: (r: FlowRun) => void;
}> = ({ flows, runs, selectedRun, onSelectRun, className }) => (
<Card className={className}>
<CardHeader>
<CardTitle>Runs</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Agent</TableHead>
<TableHead>Started</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runs.map((run) => (
<TableRow
key={run.id}
className="cursor-pointer"
onClick={() => onSelectRun(run)}
data-state={selectedRun?.id == run.id ? "selected" : null}
>
<TableCell>
{flows.find((f) => f.id == run.graphID)!.name}
</TableCell>
<TableCell>{moment(run.startTime).format("HH:mm")}</TableCell>
<TableCell>
<FlowRunStatusBadge status={run.status} />
</TableCell>
<TableCell>{formatDuration(run.duration)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
const FlowRunStatusBadge: React.FC<{
status: FlowRun["status"];
className?: string;
}> = ({ status, className }) => (
<Badge
variant="default"
className={cn(
status === "running"
? "bg-blue-500 dark:bg-blue-700"
: status === "waiting"
? "bg-yellow-500 dark:bg-yellow-600"
: status === "success"
? "bg-green-500 dark:bg-green-600"
: "bg-red-500 dark:bg-red-700",
className,
)}
>
{status}
</Badge>
);
const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta;
flowRuns: FlowRun[];
flowVersion?: number | "all";
}
> = ({ flow, flowRuns, flowVersion, ...props }) => {
const api = new AutoGPTServerAPI();
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(null);
const [selectedVersion, setSelectedFlowVersion] = useState(
flowVersion ?? "all",
);
const selectedFlowVersion: Graph | undefined = flowVersions?.find(
(v) =>
v.version == (selectedVersion == "all" ? flow.version : selectedVersion),
);
useEffect(() => {
api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result));
}, [flow.id]);
return (
<Card {...props}>
<CardHeader className="flex-row justify-between space-y-0 space-x-3">
<div>
<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 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ClockIcon className="mr-2" />
{selectedVersion == "all"
? "All versions"
: `Version ${selectedVersion}`}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Choose a version</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={String(selectedVersion)}
onValueChange={(choice) =>
setSelectedFlowVersion(
choice == "all" ? choice : Number(choice),
)
}
>
<DropdownMenuRadioItem value="all">
All versions
</DropdownMenuRadioItem>
{flowVersions?.map((v) => (
<DropdownMenuRadioItem
key={v.version}
value={v.version.toString()}
>
Version {v.version}
{v.is_active ? " (active)" : ""}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit
</Link>
<Button
variant="outline"
className="px-2.5"
title="Export to a JSON-file"
onClick={async () =>
exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(
(v) => v.version == selectedFlowVersion!.version,
)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`,
)
}
>
<ExitIcon />
</Button>
</div>
</CardHeader>
<CardContent>
<FlowRunsStats
flows={[selectedFlowVersion ?? flow]}
flowRuns={flowRuns.filter(
(r) =>
r.graphID == flow.id &&
(selectedVersion == "all" || r.graphVersion == selectedVersion),
)}
/>
</CardContent>
</Card>
);
};
const FlowRunInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta;
flowRun: FlowRun;
}
> = ({ flow, flowRun, ...props }) => {
if (flowRun.graphID != flow.id) {
throw new Error(
`FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`,
);
}
return (
<Card {...props}>
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<div>
<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>
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
</CardHeader>
<CardContent>
<p>
<strong>Status:</strong>{" "}
<FlowRunStatusBadge status={flowRun.status} />
</p>
<p>
<strong>Started:</strong>{" "}
{moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Finished:</strong>{" "}
{moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration (run time):</strong> {flowRun.duration} (
{flowRun.totalRunTime}) seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</CardContent>
</Card>
);
};
const FlowRunsStats: React.FC<{
flows: GraphMeta[];
flowRuns: FlowRun[];
title?: string;
className?: string;
}> = ({ flows, flowRuns, title, className }) => {
/* "dateMin": since the first flow in the dataset
* number > 0: custom date (unix timestamp)
* number < 0: offset relative to Date.now() (in seconds) */
const [statsSince, setStatsSince] = useState<number | "dataMin">(-24 * 3600);
const statsSinceTimestamp = // unix timestamp or null
typeof statsSince == "string"
? null
: statsSince < 0
? Date.now() + statsSince * 1000
: statsSince;
const filteredFlowRuns =
statsSinceTimestamp != null
? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp)
: flowRuns;
return (
<div className={className}>
<div className="flex flex-row items-center justify-between">
<CardTitle>{title || "Stats"}</CardTitle>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-2 * 3600)}
>
2h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-8 * 3600)}
>
8h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-24 * 3600)}
>
24h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-7 * 24 * 3600)}
>
7d
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant={"outline"} size="sm">
Custom
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
onSelect={(_, selectedDay) =>
setStatsSince(selectedDay.getTime())
}
initialFocus
/>
</PopoverContent>
</Popover>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince("dataMin")}
>
All
</Button>
</div>
</div>
<FlowRunsTimeline
flows={flows}
flowRuns={flowRuns}
dataMin={statsSince}
className="mt-3"
/>
<hr className="my-4" />
<div>
<p>
<strong>Total runs:</strong> {filteredFlowRuns.length}
</p>
<p>
<strong>Total run time:</strong>{" "}
{filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "}
seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</div>
</div>
);
};
const FlowRunsTimeline = ({
flows,
flowRuns,
dataMin,
className,
}: {
flows: GraphMeta[];
flowRuns: FlowRun[];
dataMin: "dataMin" | number;
className?: string;
}) => (
/* TODO: make logarithmic? */
<ResponsiveContainer width="100%" height={120} className={className}>
<ComposedChart>
<XAxis
dataKey="time"
type="number"
domain={[
typeof dataMin == "string"
? dataMin
: dataMin < 0
? Date.now() + dataMin * 1000
: dataMin,
Date.now(),
]}
allowDataOverflow={true}
tickFormatter={(unixTime) => {
const now = moment();
const time = moment(unixTime);
return now.diff(time, "hours") < 24
? time.format("HH:mm")
: time.format("YYYY-MM-DD HH:mm");
}}
name="Time"
scale="time"
/>
<YAxis
dataKey="_duration"
name="Duration (s)"
tickFormatter={(s) => (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)}
/>
<Tooltip
content={({ payload, label }) => {
if (payload && payload.length) {
const data: FlowRun & { time: number; _duration: number } =
payload[0].payload;
const flow = flows.find((f) => f.id === data.graphID);
return (
<Card className="p-2 text-xs leading-normal">
<p>
<strong>Agent:</strong> {flow ? flow.name : "Unknown"}
</p>
<p>
<strong>Status:</strong>&nbsp;
<FlowRunStatusBadge
status={data.status}
className="px-1.5 py-0"
/>
</p>
<p>
<strong>Started:</strong>{" "}
{moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration / run time:</strong>{" "}
{formatDuration(data.duration)} /{" "}
{formatDuration(data.totalRunTime)}
</p>
</Card>
);
}
return null;
}}
/>
{flows.map((flow) => (
<Scatter
key={flow.id}
data={flowRuns
.filter((fr) => fr.graphID == flow.id)
.map((fr) => ({
...fr,
time: fr.startTime + fr.totalRunTime * 1000,
_duration: fr.totalRunTime,
}))}
name={flow.name}
fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`}
/>
))}
{flowRuns.map((run) => (
<Line
key={run.id}
type="linear"
dataKey="_duration"
data={[
{ ...run, time: run.startTime, _duration: 0 },
{ ...run, time: run.endTime, _duration: run.totalRunTime },
]}
stroke={`hsl(${(hashString(run.graphID) * 137.5) % 360}, 70%, 50%)`}
strokeWidth={2}
dot={false}
legendType="none"
/>
))}
<Legend
content={<ScrollableLegend />}
wrapperStyle={{
bottom: 0,
left: 0,
right: 0,
width: "100%",
display: "flex",
justifyContent: "center",
}}
/>
</ComposedChart>
</ResponsiveContainer>
);
const ScrollableLegend: React.FC<
DefaultLegendContentProps & { className?: string }
> = ({ payload, className }) => {
return (
<div
className={cn(
"whitespace-nowrap px-4 text-sm overflow-x-auto space-x-3",
className,
)}
style={{ scrollbarWidth: "none" }}
>
{payload.map((entry, index) => {
if (entry.type == "none") return;
return (
<span key={`item-${index}`} className="inline-flex items-center">
<span
className="size-2.5 inline-block mr-1 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span>{entry.value}</span>
</span>
);
})}
</div>
);
};
function formatDuration(seconds: number): string {
return (
(seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() +
"s"
);
}
export default Monitor;

View File

@@ -32,9 +32,8 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
const [isHovered, setIsHovered] = useState(false);
const { setEdges } = useReactFlow();
const onEdgeClick = () => {
const onEdgeRemoveClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
data.clearNodesStatusAndOutput();
};
const [path, labelX, labelY] = getBezierPath({
@@ -105,7 +104,7 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`edge-label-button ${isHovered ? "visible" : ""}`}
onClick={onEdgeClick}
onClick={onEdgeRemoveClick}
>
<X className="size-4" />
</button>

View File

@@ -15,13 +15,15 @@ import {
BlockIORootSchema,
NodeExecutionResult,
} from "@/lib/autogpt-server-api/types";
import { BlockSchema } from "@/lib/types";
import { beautifyString, setNestedProperty } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import NodeHandle from "./NodeHandle";
import NodeInputField from "./NodeInputField";
import { Copy, Trash2 } from "lucide-react";
import { history } from "./history";
import NodeHandle from "./NodeHandle";
import { NodeGenericInputField } from "./node-input-components";
type ParsedKey = { key: string; index?: number };
export type CustomNodeData = {
blockType: string;
@@ -37,8 +39,8 @@ export type CustomNodeData = {
targetHandle: string;
}>;
isOutputOpen: boolean;
status?: string;
output_data?: any;
status?: NodeExecutionResult["status"];
output_data?: NodeExecutionResult["output_data"];
block_id: string;
backend_id?: string;
errors?: { [key: string]: string | null };
@@ -110,16 +112,30 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
));
};
const handleInputChange = (key: string, value: any) => {
const keys = key.split(".");
const handleInputChange = (path: string, value: any) => {
const keys = parseKeys(path);
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
let current = newValues;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
const { key: currentKey, index } = keys[i];
if (index !== undefined) {
if (!current[currentKey]) current[currentKey] = [];
if (!current[currentKey][index]) current[currentKey][index] = {};
current = current[currentKey][index];
} else {
if (!current[currentKey]) current[currentKey] = {};
current = current[currentKey];
}
}
const lastKey = keys[keys.length - 1];
if (lastKey.index !== undefined) {
if (!current[lastKey.key]) current[lastKey.key] = [];
current[lastKey.key][lastKey.index] = value;
} else {
current[lastKey.key] = value;
}
current[keys[keys.length - 1]] = value;
console.log(`Updating hardcoded values for node ${id}:`, newValues);
@@ -135,16 +151,49 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
data.setHardcodedValues(newValues);
const errors = data.errors || {};
// Remove error with the same key
setNestedProperty(errors, key, null);
setNestedProperty(errors, path, null);
data.setErrors({ ...errors });
};
// Helper function to parse keys with array indices
const parseKeys = (key: string): ParsedKey[] => {
const regex = /(\w+)|\[(\d+)\]/g;
const keys: ParsedKey[] = [];
let match;
let currentKey: string | null = null;
while ((match = regex.exec(key)) !== null) {
if (match[1]) {
if (currentKey !== null) {
keys.push({ key: currentKey });
}
currentKey = match[1];
} else if (match[2]) {
if (currentKey !== null) {
keys.push({ key: currentKey, index: parseInt(match[2], 10) });
currentKey = null;
} else {
throw new Error("Invalid key format: array index without a key");
}
}
}
if (currentKey !== null) {
keys.push({ key: currentKey });
}
return keys;
};
const getValue = (key: string) => {
const keys = key.split(".");
return keys.reduce(
(acc, k) => (acc && acc[k] !== undefined ? acc[k] : ""),
data.hardcodedValues,
);
const keys = parseKeys(key);
return keys.reduce((acc, k) => {
if (acc === undefined) return undefined;
if (k.index !== undefined) {
return Array.isArray(acc[k.key]) ? acc[k.key][k.index] : undefined;
}
return acc[k.key];
}, data.hardcodedValues as any);
};
const isHandleConnected = (key: string) => {
@@ -208,12 +257,10 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const handleHovered = () => {
setIsHovered(true);
console.log("isHovered", isHovered);
};
const handleMouseLeave = () => {
setIsHovered(false);
console.log("isHovered", isHovered);
};
const deleteNode = useCallback(() => {
@@ -274,58 +321,66 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
<div className="text-lg font-bold">
{beautifyString(data.blockType?.replace(/Block$/, "") || data.title)}
</div>
<div className="node-actions">
<div className="flex gap-[5px]">
{isHovered && (
<>
<button
className="node-action-button"
<Button
variant="outline"
size="icon"
onClick={copyNode}
title="Copy node"
>
<Copy size={18} />
</button>
<button
className="node-action-button"
</Button>
<Button
variant="outline"
size="icon"
onClick={deleteNode}
title="Delete node"
>
<Trash2 size={18} />
</button>
</Button>
</>
)}
</div>
</div>
<div className="node-content">
<div className="flex justify-between items-start gap-2">
<div>
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
return (
(isRequired || isAdvancedOpen) && (
<div key={key} onMouseOver={() => {}}>
<NodeHandle
keyName={key}
isConnected={isHandleConnected(key)}
isRequired={isRequired}
schema={schema}
side="left"
/>
{!isHandleConnected(key) && (
<NodeInputField
keyName={key}
schema={schema}
value={getValue(key)}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={data.errors?.[key]}
Object.entries(data.inputSchema.properties).map(
([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
return (
(isRequired || isAdvancedOpen) && (
<div key={propKey} onMouseOver={() => {}}>
<NodeHandle
keyName={propKey}
isConnected={isHandleConnected(propKey)}
isRequired={isRequired}
schema={propSchema}
side="left"
/>
)}
</div>
)
);
})}
{!isHandleConnected(propKey) && (
<NodeGenericInputField
className="mt-1 mb-2"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={
propSchema.title || beautifyString(propKey)
}
/>
)}
</div>
)
);
},
)}
</div>
<div>
<div className="flex-none">
{data.outputSchema && generateOutputHandles(data.outputSchema)}
</div>
</div>
@@ -355,14 +410,11 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
</div>
)}
<div className="flex items-center mt-2.5">
<Switch onCheckedChange={toggleOutput} className="custom-switch" />
<Switch onCheckedChange={toggleOutput} />
<span className="m-1 mr-4">Output</span>
{hasOptionalFields() && (
<>
<Switch
onCheckedChange={toggleAdvancedSettings}
className="custom-switch"
/>
<Switch onCheckedChange={toggleAdvancedSettings} />
<span className="m-1">Advanced</span>
</>
)}

View File

@@ -23,7 +23,6 @@ import CustomNode, { CustomNodeData } from "./CustomNode";
import "./flow.css";
import AutoGPTServerAPI, {
Block,
BlockIOSchema,
Graph,
NodeExecutionResult,
} from "@/lib/autogpt-server-api";
@@ -458,7 +457,6 @@ const FlowEditor: React.FC<{
targetHandle: link.sink_name,
})),
isOutputOpen: false,
setIsAnyModalOpen: setIsAnyModalOpen, // Pass setIsAnyModalOpen function
setErrors: (errors: { [key: string]: string | null }) => {
setNodes((nds) =>
nds.map((node) =>
@@ -502,11 +500,7 @@ const FlowEditor: React.FC<{
);
}
const prepareNodeInputData = (
node: Node<CustomNodeData>,
allNodes: Node<CustomNodeData>[],
allEdges: Edge<CustomEdgeData>[],
) => {
const prepareNodeInputData = (node: Node<CustomNodeData>) => {
console.log("Preparing input data for node:", node.id, node.data.blockType);
const blockSchema = availableNodes.find(
@@ -519,7 +513,7 @@ const FlowEditor: React.FC<{
}
const getNestedData = (
schema: BlockIOSchema,
schema: BlockIOSubSchema,
values: { [key: string]: any },
): { [key: string]: any } => {
let inputData: { [key: string]: any } = {};
@@ -580,7 +574,7 @@ const FlowEditor: React.FC<{
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
const inputDefault = prepareNodeInputData(node, nodes, edges);
const inputDefault = prepareNodeInputData(node);
const inputNodes = edges
.filter((edge) => edge.target === node.id)
.map((edge) => ({
@@ -685,7 +679,10 @@ const FlowEditor: React.FC<{
// Populate errors if validation fails
validate.errors?.forEach((error) => {
// Skip error if there's an edge connected
const path = error.instancePath || error.schemaPath;
const path =
"dataPath" in error
? (error.dataPath as string)
: error.instancePath;
const handle = path.split(/[\/.]/)[0];
if (
node.data.connections.some(
@@ -845,17 +842,17 @@ const FlowEditor: React.FC<{
const editorControls: Control[] = [
{
label: "Undo",
icon: <Undo2 />,
icon: <Undo2 size={18} />,
onClick: handleUndo,
},
{
label: "Redo",
icon: <Redo2 />,
icon: <Redo2 size={18} />,
onClick: handleRedo,
},
{
label: "Run",
icon: <Play />,
icon: <Play size={18} />,
onClick: runAgent,
},
];
@@ -883,17 +880,15 @@ const FlowEditor: React.FC<{
onNodeDragStart={onNodesChangeStart}
onNodeDragStop={onNodesChangeEnd}
>
<div className={"flex flex-row absolute z-10 gap-2"}>
<ControlPanel controls={editorControls}>
<BlocksControl blocks={availableNodes} addBlock={addNode} />
<SaveControl
agentMeta={savedAgent}
onSave={saveAgent}
onDescriptionChange={setAgentDescription}
onNameChange={setAgentName}
/>
</ControlPanel>
</div>
<ControlPanel className="absolute z-10" controls={editorControls}>
<BlocksControl blocks={availableNodes} addBlock={addNode} />
<SaveControl
agentMeta={savedAgent}
onSave={saveAgent}
onDescriptionChange={setAgentDescription}
onNameChange={setAgentName}
/>
</ControlPanel>
</ReactFlow>
</div>
);

View File

@@ -1,4 +1,4 @@
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { Handle, Position } from "reactflow";
@@ -6,7 +6,7 @@ import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
keyName: string;
schema: BlockIOSchema;
schema: BlockIOSubSchema;
isConnected: boolean;
isRequired?: boolean;
side: "left" | "right";
@@ -28,7 +28,7 @@ const NodeHandle: FC<HandleProps> = ({
null: "null",
};
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === "left" ? "text-left" : "text-right"}`;
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${side === "left" ? "text-left" : "text-right"}`;
const label = (
<div className="flex flex-col flex-grow">
@@ -36,13 +36,13 @@ const NodeHandle: FC<HandleProps> = ({
{schema.title || beautifyString(keyName)}
{isRequired ? "*" : ""}
</span>
<span className={typeClass}>{typeName[schema.type]}</span>
<span className={typeClass}>{typeName[schema.type] || "any"}</span>
</div>
);
const dot = (
<div
className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : "bg-gray-600"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type || "any") : "bg-gray-600"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
/>
);
@@ -53,7 +53,7 @@ const NodeHandle: FC<HandleProps> = ({
type="target"
position={Position.Left}
id={keyName}
className="group -ml-[29px]"
className="group -ml-[26px]"
>
<div className="pointer-events-none flex items-center">
{dot}
@@ -70,7 +70,7 @@ const NodeHandle: FC<HandleProps> = ({
type="source"
position={Position.Right}
id={keyName}
className="group -mr-[29px]"
className="group -mr-[26px]"
>
<div className="pointer-events-none flex items-center">
{label}

View File

@@ -1,357 +0,0 @@
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import { beautifyString } from "@/lib/utils";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
type BlockInputFieldProps = {
keyName: string;
schema: BlockIOSchema;
parentKey?: string;
value: string | Array<string> | { [key: string]: string };
handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void;
errors?: { [key: string]: string } | string | null;
};
const NodeInputField: FC<BlockInputFieldProps> = ({
keyName: key,
schema,
parentKey = "",
value,
handleInputClick,
handleInputChange,
errors,
}) => {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = typeof errors === "string" ? errors : (errors?.[key] ?? "");
const displayKey = schema.title || beautifyString(key);
const [keyValuePairs, _setKeyValuePairs] = useState<
{ key: string; value: string }[]
>(
"additionalProperties" in schema && value
? Object.entries(value).map(([key, value]) => ({
key: key,
value: value,
}))
: [],
);
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
_setKeyValuePairs(newKVPairs);
handleInputChange(
fullKey,
newKVPairs.reduce(
(obj, { key, value }) => ({ ...obj, [key]: value }),
{},
),
);
}
const renderClickableInput = (
value: string | null = null,
placeholder: string = "",
secret: boolean = false,
) => {
const className = `clickable-input ${error ? "border-error" : ""}`;
return secret ? (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value ? (
<span>********</span>
) : (
<i className="text-gray-500">{placeholder}</i>
)}
</div>
) : (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value || <i className="text-gray-500">{placeholder}</i>}
</div>
);
};
if ("properties" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{Object.entries(schema.properties).map(([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if (schema.type === "object" && schema.additionalProperties) {
return (
<div key={fullKey}>
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div
key={index}
className="flex items-center w-[325px] space-x-2 mb-2"
>
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
}),
)
}
/>
<Input
type="text"
placeholder="Value"
value={value}
onChange={(e) =>
setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: e.target.value,
}),
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() =>
setKeyValuePairs(keyValuePairs.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
))}
<Button
className="w-full"
onClick={() =>
setKeyValuePairs(keyValuePairs.concat({ key: "", value: "" }))
}
>
<PlusIcon className="mr-2" /> Add Property
</Button>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
}
if ("anyOf" in schema) {
const types = schema.anyOf.map((s) => ("type" in s ? s.type : undefined));
if (types.includes("string") && types.includes("null")) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey} (optional)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if ("allOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.allOf[0] &&
Object.entries(schema.allOf[0].properties).map(
([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
),
)}
</div>
);
}
if ("oneOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.oneOf[0] &&
Object.entries(schema.oneOf[0].properties).map(
([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
),
)}
</div>
);
}
if (!("type" in schema)) {
console.warn(`Schema for input ${key} does not specify a type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
switch (schema.type) {
case "string":
if (schema.enum) {
return (
<div key={fullKey} className="input-container">
<select
value={(value as string) || ""}
onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input"
>
<option value="">Select {displayKey}</option>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{beautifyString(option)}
</option>
))}
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
}
if (schema.secret) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey}`,
true,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey}`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
case "boolean":
return (
<div key={fullKey} className="input-container">
<select
value={value === undefined ? "" : value.toString()}
onChange={(e) =>
handleInputChange(fullKey, e.target.value === "true")
}
className="select-input"
>
<option value="">Select {displayKey}</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
case "number":
case "integer":
return (
<div key={fullKey} className="input-container">
<Input
type="number"
value={(value as string) || ""}
onChange={(e) =>
handleInputChange(fullKey, parseFloat(e.target.value))
}
className={`number-input ${error ? "border-error" : ""}`}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
case "array":
if (schema.items && schema.items.type === "string") {
const arrayValues = (value as Array<string>) || [];
return (
<div key={fullKey} className="input-container">
{arrayValues.map((item: string, index: number) => (
<div key={`${fullKey}.${index}`} className="array-item-container">
<Input
type="text"
value={item}
onChange={(e) =>
handleInputChange(`${fullKey}.${index}`, e.target.value)
}
className="array-item-input"
/>
<Button
onClick={() => handleInputChange(`${fullKey}.${index}`, "")}
className="array-item-remove"
>
&times;
</Button>
</div>
))}
<Button
onClick={() => handleInputChange(fullKey, [...arrayValues, ""])}
className="array-item-add"
>
Add Item
</Button>
{error && <span className="error-message ml-2">{error}</span>}
</div>
);
}
return null;
default:
console.warn(`Schema for input ${key} specifies unknown type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder ||
`Enter ${beautifyString(displayKey)} (Complex)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
};
export default NodeInputField;

View File

@@ -4,11 +4,11 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { Info } from "lucide-react";
import ReactMarkdown from "react-markdown";
const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => {
const SchemaTooltip: React.FC<{ schema: BlockIOSubSchema }> = ({ schema }) => {
if (!schema.description) return null;
return (

View File

@@ -0,0 +1,59 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "./ui/button";
import { Megaphone } from "lucide-react";
const TallyPopupSimple = () => {
const [isFormVisible, setIsFormVisible] = useState(false);
useEffect(() => {
// Load Tally script
const script = document.createElement("script");
script.src = "https://tally.so/widgets/embed.js";
script.async = true;
document.head.appendChild(script);
// Setup event listeners for Tally events
const handleTallyMessage = (event: MessageEvent) => {
if (typeof event.data === "string") {
try {
const data = JSON.parse(event.data);
if (data.event === "Tally.FormLoaded") {
setIsFormVisible(true);
} else if (data.event === "Tally.PopupClosed") {
setIsFormVisible(false);
}
} catch (error) {
console.error("Error parsing Tally message:", error);
}
}
};
window.addEventListener("message", handleTallyMessage);
return () => {
document.head.removeChild(script);
window.removeEventListener("message", handleTallyMessage);
};
}, []);
if (isFormVisible) {
return null; // Hide the button when the form is visible
}
return (
<div className="fixed bottom-6 right-6 p-3 transition-all duration-300 ease-in-out z-50">
<Button
variant="default"
data-tally-open="3yx2L0"
data-tally-emoji-text="👋"
data-tally-emoji-animation="wave"
>
<Megaphone />
<span className="sr-only">Reach Out</span>
</Button>
</div>
);
};
export default TallyPopupSimple;

View File

@@ -1,5 +1,5 @@
.custom-node {
padding: 15px;
@apply p-3;
border: 3px solid #4b5563;
border-radius: 12px;
background: #ffffff;
@@ -9,13 +9,6 @@
transition: border-color 0.3s ease-in-out;
}
.node-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1px;
}
.custom-node .mb-2 {
display: flex;
justify-content: space-between;
@@ -30,45 +23,6 @@
margin-right: 10px;
}
.node-actions {
display: flex;
gap: 5px;
}
.node-action-button {
width: 32px;
/* Increased size */
height: 32px;
/* Increased size */
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
/* Light gray background */
border: 1px solid #d1d5db;
/* Light border */
border-radius: 6px;
color: #4b5563;
transition: all 0.2s ease-in-out;
cursor: pointer;
}
.node-action-button:hover {
background-color: #e5e7eb;
color: #1f2937;
}
.node-action-button:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
.node-action-button svg {
width: 18px;
/* Increased icon size */
height: 18px;
/* Increased icon size */
}
/* Existing styles */
.handle-container {
display: flex;
@@ -89,38 +43,10 @@
transform: none;
}
.input-container {
margin-bottom: 5px;
}
.clickable-input {
padding: 5px;
width: 325px;
border-radius: 4px;
background: #ffffff;
border: 1px solid #d1d1d1;
color: #000000;
cursor: pointer;
word-break: break-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
}
.border-error {
border: 1px solid #d9534f;
}
.clickable-input span {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 100px);
vertical-align: middle;
}
.select-input {
width: 100%;
padding: 5px;
@@ -191,29 +117,9 @@
.error-message {
color: #d9534f;
font-size: 12px;
font-size: 13px;
margin-top: 5px;
}
.object-input {
margin-left: 10px;
border-left: 1px solid #000; /* Border for nested inputs */
padding-left: 10px;
}
.nested-input {
margin-top: 5px;
}
.key-value-input {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
}
.key-value-input input {
flex-grow: 1;
margin-left: 5px;
}
/* Styles for node states */
@@ -240,3 +146,13 @@
.custom-switch {
padding-left: 2px;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}

View File

@@ -40,8 +40,10 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
return (
<Popover>
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
<ToyBrick className="size-4" />
<PopoverTrigger asChild>
<Button size="icon" variant="ghost">
<ToyBrick size={18} />
</Button>
</PopoverTrigger>
<PopoverContent
side="right"

View File

@@ -6,6 +6,7 @@ import {
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import React from "react";
/**
@@ -24,6 +25,7 @@ export type Control = {
interface ControlPanelProps {
controls: Control[];
children?: React.ReactNode;
className?: string;
}
/**
@@ -31,11 +33,16 @@ interface ControlPanelProps {
* @param {Object} ControlPanelProps - The properties of the control panel component.
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
* @param {Array} ControlPanelProps.children - The child components of the control panel.
* @param {string} ControlPanelProps.className - Additional CSS class names for the control panel.
* @returns The rendered control panel component.
*/
export const ControlPanel = ({ controls, children }: ControlPanelProps) => {
export const ControlPanel = ({
controls,
children,
className,
}: ControlPanelProps) => {
return (
<aside className="hidden w-14 flex-col sm:flex">
<aside className={cn("hidden w-14 flex-col sm:flex", className)}>
<Card>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">

View File

@@ -51,8 +51,10 @@ export const SaveControl = ({
return (
<Popover>
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
<Save className="size-4" />
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<Save size={18} />
</Button>
</PopoverTrigger>
<PopoverContent side="right" sideOffset={15} align="start">
<Card className="border-none shadow-none">

View File

@@ -11,20 +11,6 @@ code {
monospace;
}
button {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #666;
}
input,
textarea {
background-color: #ffffff;
@@ -128,24 +114,3 @@ textarea::placeholder {
width: 100%;
height: 600px; /* Adjust this height as needed */
}
.flow-wrapper {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.flow-controls {
position: absolute;
left: -80px;
z-index: 1001;
display: flex;
gap: 10px;
transition: transform 0.3s ease;
}
.flow-controls.open {
transform: translateX(350px);
}

View File

@@ -0,0 +1,187 @@
import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api";
import React, { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDownIcon, EnterIcon } from "@radix-ui/react-icons";
import { AgentImportForm } from "@/components/agent-import-form";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import moment from "moment/moment";
import { FlowRun } from "@/lib/types";
export const AgentFlowList = ({
flows,
flowRuns,
selectedFlow,
onSelectFlow,
className,
}: {
flows: GraphMeta[];
flowRuns?: FlowRun[];
selectedFlow: GraphMeta | null;
onSelectFlow: (f: GraphMeta) => void;
className?: string;
}) => {
const [templates, setTemplates] = useState<GraphMeta[]>([]);
const api = new AutoGPTServerAPI();
useEffect(() => {
api.listTemplates().then((templates) => setTemplates(templates));
}, []);
return (
<Card className={className}>
<CardHeader className="flex-row justify-between items-center space-x-3 space-y-0">
<CardTitle>Agents</CardTitle>
<div className="flex items-center">
{/* Split "Create" button */}
<Button variant="outline" className="rounded-r-none" asChild>
<Link href="/build">Create</Link>
</Button>
<Dialog>
{/* https://ui.shadcn.com/docs/components/dialog#notes */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className={"rounded-l-none border-l-0 px-2"}
>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DialogTrigger asChild>
<DropdownMenuItem>
<EnterIcon className="mr-2" /> Import from file
</DropdownMenuItem>
</DialogTrigger>
{templates.length > 0 && (
<>
{/* List of templates */}
<DropdownMenuSeparator />
<DropdownMenuLabel>Use a template</DropdownMenuLabel>
{templates.map((template) => (
<DropdownMenuItem
key={template.id}
onClick={() => {
api
.createGraph(template.id, template.version)
.then((newGraph) => {
window.location.href = `/build?flowID=${newGraph.id}`;
});
}}
>
{template.name}
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader className="text-lg">
Import an Agent (template) from a file
</DialogHeader>
<AgentImportForm />
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
{/* <TableHead>Status</TableHead> */}
{/* <TableHead>Last updated</TableHead> */}
{flowRuns && (
<TableHead className="md:hidden lg:table-cell">
# of runs
</TableHead>
)}
{flowRuns && <TableHead>Last run</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{flows
.map((flow) => {
let runCount = 0,
lastRun: FlowRun | null = null;
if (flowRuns) {
const _flowRuns = flowRuns.filter(
(r) => r.graphID == flow.id,
);
runCount = _flowRuns.length;
lastRun =
runCount == 0
? null
: _flowRuns.reduce((a, c) =>
a.startTime > c.startTime ? a : c,
);
}
return { flow, runCount, lastRun };
})
.sort((a, b) => {
if (!a.lastRun && !b.lastRun) return 0;
if (!a.lastRun) return 1;
if (!b.lastRun) return -1;
return b.lastRun.startTime - a.lastRun.startTime;
})
.map(({ flow, runCount, lastRun }) => (
<TableRow
key={flow.id}
className="cursor-pointer"
onClick={() => onSelectFlow(flow)}
data-state={selectedFlow?.id == flow.id ? "selected" : null}
>
<TableCell>{flow.name}</TableCell>
{/* <TableCell><FlowStatusBadge status={flow.status ?? "active"} /></TableCell> */}
{/* <TableCell>
{flow.updatedAt ?? "???"}
</TableCell> */}
{flowRuns && (
<TableCell className="md:hidden lg:table-cell">
{runCount}
</TableCell>
)}
{flowRuns &&
(!lastRun ? (
<TableCell />
) : (
<TableCell title={moment(lastRun.startTime).toString()}>
{moment(lastRun.startTime).fromNow()}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
export default AgentFlowList;

View File

@@ -0,0 +1,134 @@
import React, { useEffect, useState } from "react";
import AutoGPTServerAPI, {
Graph,
GraphMeta,
safeCopyGraph,
} from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
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 { FlowRunsStats } from "@/components/monitor/index";
export const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta;
flowRuns: FlowRun[];
flowVersion?: number | "all";
}
> = ({ flow, flowRuns, flowVersion, ...props }) => {
const api = new AutoGPTServerAPI();
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(null);
const [selectedVersion, setSelectedFlowVersion] = useState(
flowVersion ?? "all",
);
const selectedFlowVersion: Graph | undefined = flowVersions?.find(
(v) =>
v.version == (selectedVersion == "all" ? flow.version : selectedVersion),
);
useEffect(() => {
api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result));
}, [flow.id]);
return (
<Card {...props}>
<CardHeader className="flex-row justify-between space-y-0 space-x-3">
<div>
<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 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ClockIcon className="mr-2" />
{selectedVersion == "all"
? "All versions"
: `Version ${selectedVersion}`}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Choose a version</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={String(selectedVersion)}
onValueChange={(choice) =>
setSelectedFlowVersion(
choice == "all" ? choice : Number(choice),
)
}
>
<DropdownMenuRadioItem value="all">
All versions
</DropdownMenuRadioItem>
{flowVersions?.map((v) => (
<DropdownMenuRadioItem
key={v.version}
value={v.version.toString()}
>
Version {v.version}
{v.is_active ? " (active)" : ""}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit
</Link>
<Button
variant="outline"
className="px-2.5"
title="Export to a JSON-file"
onClick={async () =>
exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(
(v) => v.version == selectedFlowVersion!.version,
)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`,
)
}
>
<ExitIcon />
</Button>
</div>
</CardHeader>
<CardContent>
<FlowRunsStats
flows={[selectedFlowVersion ?? flow]}
flowRuns={flowRuns.filter(
(r) =>
r.graphID == flow.id &&
(selectedVersion == "all" || r.graphVersion == selectedVersion),
)}
/>
</CardContent>
</Card>
);
};
export default FlowInfo;

View File

@@ -0,0 +1,66 @@
import React from "react";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { Pencil2Icon } from "@radix-ui/react-icons";
import moment from "moment/moment";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
export const FlowRunInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta;
flowRun: FlowRun;
}
> = ({ flow, flowRun, ...props }) => {
if (flowRun.graphID != flow.id) {
throw new Error(
`FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`,
);
}
return (
<Card {...props}>
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<div>
<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>
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
</CardHeader>
<CardContent>
<p>
<strong>Status:</strong>{" "}
<FlowRunStatusBadge status={flowRun.status} />
</p>
<p>
<strong>Started:</strong>{" "}
{moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Finished:</strong>{" "}
{moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration (run time):</strong> {flowRun.duration} (
{flowRun.totalRunTime}) seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</CardContent>
</Card>
);
};
export default FlowRunInfo;

View File

@@ -0,0 +1,25 @@
import React from "react";
import { FlowRun } from "@/lib/types";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export const FlowRunStatusBadge: React.FC<{
status: FlowRun["status"];
className?: string;
}> = ({ status, className }) => (
<Badge
variant="default"
className={cn(
status === "running"
? "bg-blue-500 dark:bg-blue-700"
: status === "waiting"
? "bg-yellow-500 dark:bg-yellow-600"
: status === "success"
? "bg-green-500 dark:bg-green-600"
: "bg-red-500 dark:bg-red-700",
className,
)}
>
{status}
</Badge>
);

View File

@@ -0,0 +1,68 @@
import React from "react";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import moment from "moment/moment";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
export const FlowRunsList: React.FC<{
flows: GraphMeta[];
runs: FlowRun[];
className?: string;
selectedRun?: FlowRun | null;
onSelectRun: (r: FlowRun) => void;
}> = ({ flows, runs, selectedRun, onSelectRun, className }) => (
<Card className={className}>
<CardHeader>
<CardTitle>Runs</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Agent</TableHead>
<TableHead>Started</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runs.map((run) => (
<TableRow
key={run.id}
className="cursor-pointer"
onClick={() => onSelectRun(run)}
data-state={selectedRun?.id == run.id ? "selected" : null}
>
<TableCell>
{flows.find((f) => f.id == run.graphID)!.name}
</TableCell>
<TableCell>{moment(run.startTime).format("HH:mm")}</TableCell>
<TableCell>
<FlowRunStatusBadge status={run.status} />
</TableCell>
<TableCell>{formatDuration(run.duration)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
function formatDuration(seconds: number): string {
return (
(seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() +
"s"
);
}
export default FlowRunsList;

View File

@@ -0,0 +1,114 @@
import React, { useState } from "react";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline";
export const FlowRunsStatus: React.FC<{
flows: GraphMeta[];
flowRuns: FlowRun[];
title?: string;
className?: string;
}> = ({ flows, flowRuns, title, className }) => {
/* "dateMin": since the first flow in the dataset
* number > 0: custom date (unix timestamp)
* number < 0: offset relative to Date.now() (in seconds) */
const [statsSince, setStatsSince] = useState<number | "dataMin">(-24 * 3600);
const statsSinceTimestamp = // unix timestamp or null
typeof statsSince == "string"
? null
: statsSince < 0
? Date.now() + statsSince * 1000
: statsSince;
const filteredFlowRuns =
statsSinceTimestamp != null
? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp)
: flowRuns;
return (
<div className={className}>
<div className="flex flex-row items-center justify-between">
<CardTitle>{title || "Stats"}</CardTitle>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-2 * 3600)}
>
2h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-8 * 3600)}
>
8h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-24 * 3600)}
>
24h
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince(-7 * 24 * 3600)}
>
7d
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant={"outline"} size="sm">
Custom
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
onSelect={(_, selectedDay) =>
setStatsSince(selectedDay.getTime())
}
initialFocus
/>
</PopoverContent>
</Popover>
<Button
variant="outline"
size="sm"
onClick={() => setStatsSince("dataMin")}
>
All
</Button>
</div>
</div>
<FlowRunsTimeline
flows={flows}
flowRuns={flowRuns}
dataMin={statsSince}
className="mt-3"
/>
<hr className="my-4" />
<div>
<p>
<strong>Total runs:</strong> {filteredFlowRuns.length}
</p>
<p>
<strong>Total run time:</strong>{" "}
{filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "}
seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</div>
</div>
);
};
export default FlowRunsStatus;

View File

@@ -0,0 +1,172 @@
import { GraphMeta } from "@/lib/autogpt-server-api";
import {
ComposedChart,
DefaultLegendContentProps,
Legend,
Line,
ResponsiveContainer,
Scatter,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import moment from "moment/moment";
import { Card } from "@/components/ui/card";
import { cn, hashString } from "@/lib/utils";
import React from "react";
import { FlowRun } from "@/lib/types";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
export const FlowRunsTimeline = ({
flows,
flowRuns,
dataMin,
className,
}: {
flows: GraphMeta[];
flowRuns: FlowRun[];
dataMin: "dataMin" | number;
className?: string;
}) => (
/* TODO: make logarithmic? */
<ResponsiveContainer width="100%" height={120} className={className}>
<ComposedChart>
<XAxis
dataKey="time"
type="number"
domain={[
typeof dataMin == "string"
? dataMin
: dataMin < 0
? Date.now() + dataMin * 1000
: dataMin,
Date.now(),
]}
allowDataOverflow={true}
tickFormatter={(unixTime) => {
const now = moment();
const time = moment(unixTime);
return now.diff(time, "hours") < 24
? time.format("HH:mm")
: time.format("YYYY-MM-DD HH:mm");
}}
name="Time"
scale="time"
/>
<YAxis
dataKey="_duration"
name="Duration (s)"
tickFormatter={(s) => (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)}
/>
<Tooltip
content={({ payload, label }) => {
if (payload && payload.length) {
const data: FlowRun & { time: number; _duration: number } =
payload[0].payload;
const flow = flows.find((f) => f.id === data.graphID);
return (
<Card className="p-2 text-xs leading-normal">
<p>
<strong>Agent:</strong> {flow ? flow.name : "Unknown"}
</p>
<p>
<strong>Status:</strong>&nbsp;
<FlowRunStatusBadge
status={data.status}
className="px-1.5 py-0"
/>
</p>
<p>
<strong>Started:</strong>{" "}
{moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration / run time:</strong>{" "}
{formatDuration(data.duration)} /{" "}
{formatDuration(data.totalRunTime)}
</p>
</Card>
);
}
return null;
}}
/>
{flows.map((flow) => (
<Scatter
key={flow.id}
data={flowRuns
.filter((fr) => fr.graphID == flow.id)
.map((fr) => ({
...fr,
time: fr.startTime + fr.totalRunTime * 1000,
_duration: fr.totalRunTime,
}))}
name={flow.name}
fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`}
/>
))}
{flowRuns.map((run) => (
<Line
key={run.id}
type="linear"
dataKey="_duration"
data={[
{ ...run, time: run.startTime, _duration: 0 },
{ ...run, time: run.endTime, _duration: run.totalRunTime },
]}
stroke={`hsl(${(hashString(run.graphID) * 137.5) % 360}, 70%, 50%)`}
strokeWidth={2}
dot={false}
legendType="none"
/>
))}
<Legend
content={<ScrollableLegend />}
wrapperStyle={{
bottom: 0,
left: 0,
right: 0,
width: "100%",
display: "flex",
justifyContent: "center",
}}
/>
</ComposedChart>
</ResponsiveContainer>
);
export default FlowRunsTimeline;
const ScrollableLegend: React.FC<
DefaultLegendContentProps & { className?: string }
> = ({ payload, className }) => {
return (
<div
className={cn(
"whitespace-nowrap px-4 text-sm overflow-x-auto space-x-3",
className,
)}
style={{ scrollbarWidth: "none" }}
>
{payload?.map((entry, index) => {
if (entry.type == "none") return;
return (
<span key={`item-${index}`} className="inline-flex items-center">
<span
className="size-2.5 inline-block mr-1 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span>{entry.value}</span>
</span>
);
})}
</div>
);
};
function formatDuration(seconds: number): string {
return (
(seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() +
"s"
);
}

View File

@@ -0,0 +1,6 @@
export { default as AgentFlowList } from "./AgentFlowList";
export { default as FlowRunsList } from "./FlowRunsList";
export { default as FlowInfo } from "./FlowInfo";
export { default as FlowRunInfo } from "./FlowRunInfo";
export { default as FlowRunsStats } from "./FlowRunsStatus";
export { default as FlowRunsTimeline } from "./FlowRunsTimeline";

View File

@@ -0,0 +1,616 @@
import { Cross2Icon, Pencil2Icon, PlusIcon } from "@radix-ui/react-icons";
import { beautifyString, cn } from "@/lib/utils";
import {
BlockIORootSchema,
BlockIOSubSchema,
BlockIOObjectSubSchema,
BlockIOKVSubSchema,
BlockIOArraySubSchema,
BlockIOStringSubSchema,
BlockIONumberSubSchema,
BlockIOBooleanSubSchema,
} from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { Input } from "./ui/input";
type NodeObjectInputTreeProps = {
selfKey?: string;
schema: BlockIORootSchema | BlockIOObjectSubSchema;
object?: { [key: string]: any };
handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void;
errors: { [key: string]: string | undefined };
className?: string;
displayName?: string;
};
const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
selfKey = "",
schema,
object,
handleInputClick,
handleInputChange,
errors,
className,
displayName,
}) => {
object ??= ("default" in schema ? schema.default : null) ?? {};
return (
<div className={cn(className, "flex-col w-full")}>
{displayName && <strong>{displayName}</strong>}
{Object.entries(schema.properties).map(([propKey, propSchema]) => {
const childKey = selfKey ? `${selfKey}.${propKey}` : propKey;
return (
<div
key={propKey}
className="flex flex-row justify-between space-y-2 w-full"
>
<span className="mr-2 mt-3">
{propSchema.title || beautifyString(propKey)}
</span>
<NodeGenericInputField
key={propKey}
propKey={childKey}
propSchema={propSchema}
currentValue={object ? object[propKey] : undefined}
errors={errors}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
displayName={propSchema.title || beautifyString(propKey)}
/>
</div>
);
})}
</div>
);
};
export default NodeObjectInputTree;
export const NodeGenericInputField: FC<{
propKey: string;
propSchema: BlockIOSubSchema;
currentValue?: any;
errors: NodeObjectInputTreeProps["errors"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName?: string;
}> = ({
propKey,
propSchema,
currentValue,
errors,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
displayName ??= propSchema.title || beautifyString(propKey);
if ("allOf" in propSchema) {
// If this happens, that is because Pydantic wraps $refs in an allOf if the
// $ref has sibling schema properties (which isn't technically allowed),
// so there will only be one item in allOf[].
// AFAIK this should NEVER happen though, as $refs are resolved server-side.
propSchema = propSchema.allOf[0];
console.warn(`Unsupported 'allOf' in schema for '${propKey}'!`, propSchema);
}
if ("properties" in propSchema) {
return (
<NodeObjectInputTree
selfKey={propKey}
schema={propSchema}
object={currentValue}
errors={errors}
className={cn("border-l border-gray-500 pl-2", className)} // visual indent
displayName={displayName}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
/>
);
}
if ("additionalProperties" in propSchema) {
return (
<NodeKeyValueInput
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
}
if ("anyOf" in propSchema) {
// optional items
const types = propSchema.anyOf.map((s) =>
"type" in s ? s.type : undefined,
);
if (types.includes("string") && types.includes("null")) {
// optional string
return (
<NodeStringInput
selfKey={propKey}
schema={{ ...propSchema, type: "string" } as BlockIOStringSubSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
}
}
if ("oneOf" in propSchema) {
// At the time of writing, this isn't used in the backend -> no impl. needed
console.error(
`Unsupported 'oneOf' in schema for '${propKey}'!`,
propSchema,
);
return null;
}
if (!("type" in propSchema)) {
return (
<NodeFallbackInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
}
switch (propSchema.type) {
case "string":
return (
<NodeStringInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
case "boolean":
return (
<NodeBooleanInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
case "number":
case "integer":
return (
<NodeNumberInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
case "array":
return (
<NodeArrayInput
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
default:
console.warn(
`Schema for '${propKey}' specifies unknown type:`,
propSchema,
);
return (
<NodeFallbackInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
}
};
const NodeKeyValueInput: FC<{
selfKey: string;
schema: BlockIOKVSubSchema;
entries?: { [key: string]: string } | { [key: string]: number };
errors: { [key: string]: string | undefined };
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName?: string;
}> = ({
selfKey,
entries,
schema,
handleInputChange,
errors,
className,
displayName,
}) => {
const [keyValuePairs, setKeyValuePairs] = useState<
{
key: string;
value: string | number | null;
}[]
>(
Object.entries(entries ?? schema.default ?? {}).map(([key, value]) => ({
key,
value: value,
})),
);
function updateKeyValuePairs(newPairs: typeof keyValuePairs) {
setKeyValuePairs(newPairs);
handleInputChange(
selfKey,
newPairs.reduce((obj, { key, value }) => ({ ...obj, [key]: value }), {}),
);
}
function convertValueType(value: string): string | number | null {
if (schema.additionalProperties.type == "string") return value;
if (!value) return null;
return Number(value);
}
return (
<div className={cn(className, "flex flex-col")}>
{displayName && <strong>{displayName}</strong>}
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div key={index}>
<div className="flex items-center space-x-2 mb-2 nodrag">
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
}),
)
}
/>
<Input
type="text"
placeholder="Value"
value={value ?? ""}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: convertValueType(e.target.value),
}),
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() =>
updateKeyValuePairs(keyValuePairs.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
{errors[`${selfKey}.${key}`] && (
<span className="error-message">
{errors[`${selfKey}.${key}`]}
</span>
)}
</div>
))}
<Button
className="w-full"
onClick={() =>
updateKeyValuePairs(keyValuePairs.concat({ key: "", value: "" }))
}
>
<PlusIcon className="mr-2" /> Add Property
</Button>
</div>
{errors[selfKey] && (
<span className="error-message">{errors[selfKey]}</span>
)}
</div>
);
};
const NodeArrayInput: FC<{
selfKey: string;
schema: BlockIOArraySubSchema;
entries?: string[];
errors: { [key: string]: string | undefined };
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName?: string;
}> = ({
selfKey,
schema,
entries,
errors,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
entries ??= schema.default ?? [];
const isItemObject = "items" in schema && "properties" in schema.items!;
const error =
typeof errors[selfKey] === "string" ? errors[selfKey] : undefined;
return (
<div className={cn(className, "flex flex-col")}>
{displayName && <strong>{displayName}</strong>}
{entries.map((entry: any, index: number) => {
const entryKey = `${selfKey}[${index}]`;
return (
<div key={entryKey}>
<div className="flex items-center space-x-2 mb-2">
{schema.items ? (
<NodeGenericInputField
propKey={entryKey}
propSchema={schema.items}
currentValue={entry}
errors={errors}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
) : (
<NodeFallbackInput
selfKey={entryKey}
schema={schema.items}
value={entry}
error={errors[entryKey]}
displayName={displayName || beautifyString(selfKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
)}
<Button
variant="ghost"
size="icon"
onClick={() =>
handleInputChange(selfKey, entries.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
{errors[entryKey] && typeof errors[entryKey] === "string" && (
<span className="error-message">{errors[entryKey]}</span>
)}
</div>
);
})}
<Button
onClick={() =>
handleInputChange(selfKey, [...entries, isItemObject ? {} : ""])
}
>
<PlusIcon className="mr-2" /> Add Item
</Button>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeStringInput: FC<{
selfKey: string;
schema: BlockIOStringSubSchema;
value?: string;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName: string;
}> = ({
selfKey,
schema,
value,
error,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
return (
<div className={className}>
{schema.enum ? (
<Select
defaultValue={value}
onValueChange={(newValue) => handleInputChange(selfKey, newValue)}
>
<SelectTrigger>
<SelectValue placeholder={schema.placeholder || displayName} />
</SelectTrigger>
<SelectContent className="nodrag">
{schema.enum.map((option, index) => (
<SelectItem key={index} value={option}>
{beautifyString(option)}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div
className="nodrag relative"
onClick={schema.secret ? () => handleInputClick(selfKey) : undefined}
>
<Input
type="text"
id={selfKey}
value={schema.secret && value ? "********" : value}
readOnly={schema.secret}
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)}`
}
onChange={(e) => handleInputChange(selfKey, e.target.value)}
className="pr-8 read-only:cursor-pointer read-only:text-gray-500"
/>
<Button
variant="ghost"
size="icon"
className="absolute inset-1 left-auto h-7 w-7 rounded-[0.25rem]"
onClick={() => handleInputClick(selfKey)}
title="Open a larger textbox input"
>
<Pencil2Icon className="m-0 p-0" />
</Button>
</div>
)}
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeNumberInput: FC<{
selfKey: string;
schema: BlockIONumberSubSchema;
value?: number;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName?: string;
}> = ({
selfKey,
schema,
value,
error,
handleInputChange,
className,
displayName,
}) => {
value ??= schema.default;
displayName ??= schema.title || beautifyString(selfKey);
return (
<div className={className}>
<div className="flex items-center justify-between space-x-3 nodrag">
<Input
type="number"
id={selfKey}
value={value}
onChange={(e) =>
handleInputChange(selfKey, parseFloat(e.target.value))
}
placeholder={
schema.placeholder || `Enter ${beautifyString(displayName)}`
}
/>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeBooleanInput: FC<{
selfKey: string;
schema: BlockIOBooleanSubSchema;
value?: boolean;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName: string;
}> = ({
selfKey,
schema,
value,
error,
handleInputChange,
className,
displayName,
}) => {
value ??= schema.default ?? false;
return (
<div className={className}>
<div className="flex items-center nodrag">
<Switch
checked={value}
onCheckedChange={(v) => handleInputChange(selfKey, v)}
/>
<span className="ml-3">{displayName}</span>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeFallbackInput: FC<{
selfKey: string;
schema?: BlockIOSubSchema;
value: any;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName: string;
}> = ({
selfKey,
schema,
value,
error,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
return (
<NodeStringInput
selfKey={selfKey}
schema={{ type: "string", ...schema } as BlockIOStringSubSchema}
value={value}
error={error}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
className={className}
displayName={displayName}
/>
);
};

View File

@@ -0,0 +1,167 @@
"use client";
import * as React from "react";
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-neutral-800 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-neutral-300",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 bg-white text-neutral-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn(
"-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800",
className,
)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -9,60 +9,86 @@ export type Block = {
export type BlockIORootSchema = {
type: "object";
properties: { [key: string]: BlockIOSchema };
properties: { [key: string]: BlockIOSubSchema };
required?: string[];
additionalProperties?: { type: string };
};
export type BlockIOSchema = {
export type BlockIOSubSchema =
| BlockIOSimpleTypeSubSchema
| BlockIOCombinedTypeSubSchema;
type BlockIOSimpleTypeSubSchema =
| BlockIOObjectSubSchema
| BlockIOKVSubSchema
| BlockIOArraySubSchema
| BlockIOStringSubSchema
| BlockIONumberSubSchema
| BlockIOBooleanSubSchema
| BlockIONullSubSchema;
type BlockIOSubSchemaMeta = {
title?: string;
description?: string;
placeholder?: string;
} & (BlockIOSimpleTypeSchema | BlockIOCombinedTypeSchema);
};
type BlockIOSimpleTypeSchema =
| {
type: "object";
properties: { [key: string]: BlockIOSchema };
required?: string[];
additionalProperties?: { type: string };
}
| {
type: "array";
items?: BlockIOSimpleTypeSchema;
}
| {
type: "string";
enum?: string[];
secret?: true;
default?: string;
}
| {
type: "integer" | "number";
default?: number;
}
| {
type: "boolean";
default?: boolean;
}
| {
type: "null";
};
export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
type: "object";
properties: { [key: string]: BlockIOSubSchema };
default?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
required?: keyof BlockIOObjectSubSchema["properties"][];
};
export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
type: "object";
additionalProperties: { type: "string" | "number" | "integer" };
default?: { [key: string]: string | number };
};
export type BlockIOArraySubSchema = BlockIOSubSchemaMeta & {
type: "array";
items?: BlockIOSimpleTypeSubSchema;
default?: Array<string>;
};
export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
type: "string";
enum?: string[];
secret?: true;
default?: string;
};
export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
type: "integer" | "number";
default?: number;
};
export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & {
type: "boolean";
default?: boolean;
};
export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
type: "null";
};
// At the time of writing, combined schemas only occur on the first nested level in a
// block schema. It is typed this way to make the use of these objects less tedious.
type BlockIOCombinedTypeSchema =
| {
allOf: [BlockIOSimpleTypeSchema];
}
| {
anyOf: BlockIOSimpleTypeSchema[];
default?: string | number | boolean | null;
}
| {
oneOf: BlockIOSimpleTypeSchema[];
default?: string | number | boolean | null;
};
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta &
(
| {
allOf: [BlockIOSimpleTypeSubSchema];
}
| {
anyOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
}
| {
oneOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
}
);
/* Mirror of autogpt_server/data/graph.py:Node */
export type Node = {

View File

@@ -0,0 +1,13 @@
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
export type FlowRun = {
id: string;
graphID: string;
graphVersion: number;
status: "running" | "waiting" | "success" | "failed";
startTime: number; // unix timestamp (ms)
endTime: number; // unix timestamp (ms)
duration: number; // seconds
totalRunTime: number; // seconds
nodeExecutionResults: NodeExecutionResult[];
};

View File

@@ -45,6 +45,7 @@ export function getTypeTextColor(type: string | null): string {
object: "text-purple-500",
array: "text-indigo-500",
null: "text-gray-500",
any: "text-gray-500",
"": "text-gray-500",
}[type] || "text-gray-500"
);
@@ -61,6 +62,7 @@ export function getTypeBgColor(type: string | null): string {
object: "bg-purple-500",
array: "bg-indigo-500",
null: "bg-gray-500",
any: "bg-gray-500",
"": "bg-gray-500",
}[type] || "bg-gray-500"
);
@@ -76,6 +78,7 @@ export function getTypeColor(type: string | null): string {
object: "#a855f7",
array: "#6366f1",
null: "#6b7280",
any: "#6b7280",
"": "#6b7280",
}[type] || "#6b7280"
);

View File

@@ -466,6 +466,33 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-select@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.1.tgz#df05cb0b29d3deaef83b505917c4042e0e418a9f"
integrity sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==
dependencies:
"@radix-ui/number" "1.1.0"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-focus-guards" "1.1.0"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@radix-ui/react-scroll-area@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"

View File

@@ -1,6 +1,7 @@
import glob
import importlib
import os
import re
from pathlib import Path
from autogpt_server.data.block import Block
@@ -15,6 +16,11 @@ modules = [
if os.path.isfile(f) and f.endswith(".py") and not f.endswith("__init__.py")
]
for module in modules:
if not re.match("^[a-z_]+$", module):
raise ValueError(
f"Block module {module} error: module name must be lowercase, separated by underscores, and contain only alphabet characters"
)
importlib.import_module(f".{module}", package=__name__)
AVAILABLE_MODULES.append(module)
@@ -30,9 +36,16 @@ def all_subclasses(clz):
for cls in all_subclasses(Block):
if not cls.__name__.endswith("Block"):
name = cls.__name__
if cls.__name__.endswith("Base"):
continue
if not cls.__name__.endswith("Block"):
raise ValueError(
f"Block class {cls.__name__} does not end with 'Block', If you are creating an abstract class, please name the class with 'Base' at the end"
)
block = cls()
if not isinstance(block.id, str) or len(block.id) != 36:

View File

@@ -131,7 +131,7 @@ class WebScraperBlock(Block, GetRequest):
yield "error", f"Request to Jina-ai Reader failed: {e}"
class GetOpenWeatherMapWeather(Block, GetRequest):
class GetOpenWeatherMapBlock(Block, GetRequest):
class Input(BlockSchema):
location: str
api_key: BlockSecret = SecretField(key="openweathermap_api_key")
@@ -146,8 +146,8 @@ class GetOpenWeatherMapWeather(Block, GetRequest):
def __init__(self):
super().__init__(
id="f7a8b2c3-6d4e-5f8b-9e7f-6d4e5f8b9e7f",
input_schema=GetOpenWeatherMapWeather.Input,
output_schema=GetOpenWeatherMapWeather.Output,
input_schema=GetOpenWeatherMapBlock.Input,
output_schema=GetOpenWeatherMapBlock.Output,
test_input={
"location": "New York",
"api_key": "YOUR_API_KEY",

View File

@@ -0,0 +1,91 @@
import time
from datetime import datetime, timedelta
from typing import Union
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
class CurrentTimeBlock(Block):
class Input(BlockSchema):
trigger: str
class Output(BlockSchema):
time: str
def __init__(self):
super().__init__(
id="a892b8d9-3e4e-4e9c-9c1e-75f8efcf1bfa",
description="This block outputs the current time.",
categories={BlockCategory.TEXT},
input_schema=CurrentTimeBlock.Input,
output_schema=CurrentTimeBlock.Output,
test_input=[
{"trigger": "Hello", "format": "{time}"},
],
test_output=[
("time", time.strftime("%H:%M:%S")),
],
)
def run(self, input_data: Input) -> BlockOutput:
current_time = time.strftime("%H:%M:%S")
yield "time", current_time
class CurrentDateBlock(Block):
class Input(BlockSchema):
trigger: str
offset: Union[int, str]
class Output(BlockSchema):
date: str
def __init__(self):
super().__init__(
id="b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0b1",
description="This block outputs the current date with an optional offset.",
categories={BlockCategory.TEXT},
input_schema=CurrentDateBlock.Input,
output_schema=CurrentDateBlock.Output,
test_input=[
{"trigger": "Hello", "format": "{date}", "offset": "7"},
],
test_output=[
("date", (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")),
],
)
def run(self, input_data: Input) -> BlockOutput:
try:
offset = int(input_data.offset)
except ValueError:
offset = 0
current_date = datetime.now() - timedelta(days=offset)
yield "date", current_date.strftime("%Y-%m-%d")
class CurrentDateAndTimeBlock(Block):
class Input(BlockSchema):
trigger: str
class Output(BlockSchema):
date_time: str
def __init__(self):
super().__init__(
id="b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2",
description="This block outputs the current date and time.",
categories={BlockCategory.TEXT},
input_schema=CurrentDateAndTimeBlock.Input,
output_schema=CurrentDateAndTimeBlock.Output,
test_input=[
{"trigger": "Hello", "format": "{date_time}"},
],
test_output=[
("date_time", time.strftime("%Y-%m-%d %H:%M:%S")),
],
)
def run(self, input_data: Input) -> BlockOutput:
current_date_time = time.strftime("%Y-%m-%d %H:%M:%S")
yield "date_time", current_date_time

View File

@@ -7,7 +7,7 @@ from autogpt_server.data.block import Block, BlockOutput, BlockSchema
from autogpt_server.data.model import SchemaField
class YouTubeTranscriber(Block):
class YouTubeTranscriberBlock(Block):
class Input(BlockSchema):
youtube_url: str = SchemaField(
description="The URL of the YouTube video to transcribe",
@@ -24,8 +24,8 @@ class YouTubeTranscriber(Block):
def __init__(self):
super().__init__(
id="f3a8f7e1-4b1d-4e5f-9f2a-7c3d5a2e6b4c",
input_schema=YouTubeTranscriber.Input,
output_schema=YouTubeTranscriber.Output,
input_schema=YouTubeTranscriberBlock.Input,
output_schema=YouTubeTranscriberBlock.Output,
test_input={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
test_output=[
("video_id", "dQw4w9WgXcQ"),

View File

@@ -0,0 +1,199 @@
{
"id": "381164dd-3c91-43fd-ba93-c12a13ce8499",
"version": 5,
"is_active": false,
"is_template": true,
"name": "Discord Bot Chat To LLM",
"description": "Simply send the bot the message \"!chat <message>\" and it will reply.",
"nodes": [
{
"id": "b8138bca-7892-42c2-9594-a845d3483413",
"block_id": "d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t",
"input_default": {},
"metadata": {
"position": {
"x": -98.31744952152862,
"y": 291.1279542656707
}
}
},
{
"id": "b667bcc4-4e17-4343-bd31-14e48d99d21d",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {
"input2": " Said: "
},
"metadata": {
"position": {
"x": 642.0641136440832,
"y": -318.9010839696226
}
}
},
{
"id": "42eda7a9-fe29-45c8-9571-55222830142d",
"block_id": "3146e4fe-2cdd-4f29-bd12-0c9d5bb4deb0",
"input_default": {
"pattern": "(?<=!chat ).*"
},
"metadata": {
"position": {
"x": 651.4338270731059,
"y": 120.68871252027822
}
}
},
{
"id": "9049f063-5b07-4984-b211-068bc93e653a",
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
"input_default": {
"model": "gpt-4o",
"sys_prompt": "You are a nice friendly AI"
},
"metadata": {
"position": {
"x": 2099.785393180648,
"y": -325.6642266305269
}
}
},
{
"id": "dda2d061-2ef9-4dc5-9433-918c8395a4ac",
"block_id": "h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6",
"input_default": {},
"metadata": {
"position": {
"x": 2697.355782645,
"y": 225.29000586164966
}
}
},
{
"id": "3209c5e1-2da9-4cd1-bf4b-2f9488577815",
"block_id": "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
"input_default": {
"data": "DISCORD BOT API KEY HERE"
},
"metadata": {
"position": {
"x": -772.5858672155341,
"y": 26.390737439792503
}
}
},
{
"id": "b6411821-bd48-4543-b526-0f7138e8ffe9",
"block_id": "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
"input_default": {
"input": "DISCORD BOT API KEY HERE"
},
"metadata": {
"position": {
"x": -778.4138607648867,
"y": 422.0409097488691
}
}
},
{
"id": "d693cda1-973d-4d62-b549-d696b73d51d9",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {},
"metadata": {
"position": {
"x": 1325.5852307018679,
"y": -328.95888935525124
}
}
}
],
"links": [
{
"id": "80683364-c3e8-467b-a734-d5629f97cd30",
"source_id": "b8138bca-7892-42c2-9594-a845d3483413",
"sink_id": "42eda7a9-fe29-45c8-9571-55222830142d",
"source_name": "message_content",
"sink_name": "text",
"is_static": false
},
{
"id": "8510bd83-1444-4a70-99e3-26c3ae28d7bf",
"source_id": "42eda7a9-fe29-45c8-9571-55222830142d",
"sink_id": "3209c5e1-2da9-4cd1-bf4b-2f9488577815",
"source_name": "negative",
"sink_name": "input",
"is_static": false
},
{
"id": "ff48a673-1f18-4b05-b5e7-e6dcc3e65add",
"source_id": "b8138bca-7892-42c2-9594-a845d3483413",
"sink_id": "dda2d061-2ef9-4dc5-9433-918c8395a4ac",
"source_name": "channel_name",
"sink_name": "channel_name",
"is_static": false
},
{
"id": "aebf9b2b-ee01-41bf-9c05-6444b6e5aa44",
"source_id": "3209c5e1-2da9-4cd1-bf4b-2f9488577815",
"sink_id": "b8138bca-7892-42c2-9594-a845d3483413",
"source_name": "output",
"sink_name": "discord_bot_token",
"is_static": false
},
{
"id": "cdbf9290-1b63-463d-a869-a16734ebd03c",
"source_id": "9049f063-5b07-4984-b211-068bc93e653a",
"sink_id": "dda2d061-2ef9-4dc5-9433-918c8395a4ac",
"source_name": "response",
"sink_name": "message_content",
"is_static": false
},
{
"id": "d9a51e17-c8de-4835-bee1-c1abba457c35",
"source_id": "dda2d061-2ef9-4dc5-9433-918c8395a4ac",
"sink_id": "3209c5e1-2da9-4cd1-bf4b-2f9488577815",
"source_name": "status",
"sink_name": "input",
"is_static": false
},
{
"id": "7bea8f77-45d7-4884-974f-b8f5ad10a988",
"source_id": "b6411821-bd48-4543-b526-0f7138e8ffe9",
"sink_id": "b8138bca-7892-42c2-9594-a845d3483413",
"source_name": "output",
"sink_name": "discord_bot_token",
"is_static": false
},
{
"id": "f2427ca7-3adf-450f-8be4-b8042eb0b9a6",
"source_id": "b8138bca-7892-42c2-9594-a845d3483413",
"sink_id": "b667bcc4-4e17-4343-bd31-14e48d99d21d",
"source_name": "username",
"sink_name": "input1",
"is_static": false
},
{
"id": "117244bf-8c32-4096-baff-38cd0fa9cf9d",
"source_id": "b667bcc4-4e17-4343-bd31-14e48d99d21d",
"sink_id": "d693cda1-973d-4d62-b549-d696b73d51d9",
"source_name": "output",
"sink_name": "input1",
"is_static": false
},
{
"id": "9ee4a0a5-de27-4bf8-81a9-140db1b5e475",
"source_id": "d693cda1-973d-4d62-b549-d696b73d51d9",
"sink_id": "9049f063-5b07-4984-b211-068bc93e653a",
"source_name": "output",
"sink_name": "prompt",
"is_static": false
},
{
"id": "49da866a-8c13-469c-95ea-fe4685e95c75",
"source_id": "42eda7a9-fe29-45c8-9571-55222830142d",
"sink_id": "d693cda1-973d-4d62-b549-d696b73d51d9",
"source_name": "positive",
"sink_name": "input2",
"is_static": false
}
]
}

View File

@@ -0,0 +1,266 @@
{
"id": "696b4b9c-f28f-4dda-a44c-e748ac22438f",
"version": 17,
"is_active": false,
"is_template": true,
"name": "Discord Search Bot",
"description": "This is a Discord search bot, send it the command \"!search <question>\" and it will do a web search and answer your question!",
"nodes": [
{
"id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"block_id": "d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t",
"input_default": {},
"metadata": {
"position": {
"x": -961.2660758713816,
"y": 333.47185665649613
}
}
},
{
"id": "b09e201a-cd71-42d4-a197-22e7eebc54c9",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {
"input2": ", Here is the latest web info to answer the question : \n"
},
"metadata": {
"position": {
"x": 881.3259434267115,
"y": -564.3287840347994
}
}
},
{
"id": "3169d1a8-b541-43f7-97ce-ddc6aecb2080",
"block_id": "3146e4fe-2cdd-4f29-bd12-0c9d5bb4deb0",
"input_default": {
"pattern": "(?<=!search ).*"
},
"metadata": {
"position": {
"x": -284.1111358361005,
"y": -43.71794261767991
}
}
},
{
"id": "5658c4f7-8e67-4d30-93f2-157bdbd3ef87",
"block_id": "b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7",
"input_default": {},
"metadata": {
"position": {
"x": 319.9343851243159,
"y": -48.49947115893917
}
}
},
{
"id": "b29e3831-3fb7-41bd-88d8-ce3a5dde3d69",
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
"input_default": {
"model": "gpt-4o",
"sys_prompt": "You are a question answerer and info summariser, answer the questions with the info you are provided, be sure to @ the user who asked the question in your reply like @username"
},
"metadata": {
"position": {
"x": 2085.06017081387,
"y": -387.5334342999411
}
}
},
{
"id": "164bc3ea-e812-4391-a62d-bdddcf86f3cd",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {},
"metadata": {
"position": {
"x": 1469.6744442484253,
"y": -435.0392111332514
}
}
},
{
"id": "10759047-6387-4ff1-9117-bbef47d24ee8",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {},
"metadata": {
"position": {
"x": 326.8949613725521,
"y": -579.6877803706152
}
}
},
{
"id": "af7c5160-7bf0-4ad0-9806-04222009091f",
"block_id": "e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
"input_default": {
"input2": " Asked the question: "
},
"metadata": {
"position": {
"x": -265.6965655001714,
"y": -628.1379507780849
}
}
},
{
"id": "4d74513d-42f7-4fd0-808a-0f4844513966",
"block_id": "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
"input_default": {
"input": "DISCORD BOT API KEY HERE"
},
"metadata": {
"position": {
"x": -1532.6418163253616,
"y": 587.6533051108552
}
}
},
{
"id": "f3d62f22-d193-4f04-85d2-164200fca4c0",
"block_id": "h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6",
"input_default": {},
"metadata": {
"position": {
"x": 2814.192971071703,
"y": 310.74654561036294
}
}
},
{
"id": "3b2bb6a5-9c42-4189-a9a0-0e499ccb766a",
"block_id": "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
"input_default": {
"data": "DISCORD BOT API KEY HERE"
},
"metadata": {
"position": {
"x": -1528.6418163253616,
"y": 119.65330511085517
}
}
}
],
"links": [
{
"id": "346a8259-1093-4374-8271-904742aa6d89",
"source_id": "b29e3831-3fb7-41bd-88d8-ce3a5dde3d69",
"sink_id": "f3d62f22-d193-4f04-85d2-164200fca4c0",
"source_name": "response",
"sink_name": "message_content",
"is_static": false
},
{
"id": "53a8ecc6-60b6-4f4a-90c4-cb11dd1874e0",
"source_id": "5658c4f7-8e67-4d30-93f2-157bdbd3ef87",
"sink_id": "164bc3ea-e812-4391-a62d-bdddcf86f3cd",
"source_name": "results",
"sink_name": "input2",
"is_static": false
},
{
"id": "2b3fd279-5816-48da-b2ab-484497fe67d5",
"source_id": "f3d62f22-d193-4f04-85d2-164200fca4c0",
"sink_id": "3b2bb6a5-9c42-4189-a9a0-0e499ccb766a",
"source_name": "status",
"sink_name": "input",
"is_static": false
},
{
"id": "bb036c88-4031-4c6c-a70b-a82f5e50a013",
"source_id": "4d74513d-42f7-4fd0-808a-0f4844513966",
"sink_id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"source_name": "output",
"sink_name": "discord_bot_token",
"is_static": false
},
{
"id": "e67befdc-59b5-47bf-9663-8baeeef026f7",
"source_id": "3169d1a8-b541-43f7-97ce-ddc6aecb2080",
"sink_id": "10759047-6387-4ff1-9117-bbef47d24ee8",
"source_name": "positive",
"sink_name": "input2",
"is_static": false
},
{
"id": "9c0fa608-ceea-44cd-98cf-8a2d6ed25b24",
"source_id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"sink_id": "af7c5160-7bf0-4ad0-9806-04222009091f",
"source_name": "username",
"sink_name": "input1",
"is_static": false
},
{
"id": "ad5e1bd6-69bd-4846-87dc-e08d8d2e0f2b",
"source_id": "af7c5160-7bf0-4ad0-9806-04222009091f",
"sink_id": "10759047-6387-4ff1-9117-bbef47d24ee8",
"source_name": "output",
"sink_name": "input1",
"is_static": false
},
{
"id": "96f4b2fd-82d8-4754-9f41-f65e8e1f565a",
"source_id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"sink_id": "3169d1a8-b541-43f7-97ce-ddc6aecb2080",
"source_name": "message_content",
"sink_name": "text",
"is_static": false
},
{
"id": "ec6666bc-4d54-4960-b3b1-13a0b4a872a7",
"source_id": "3b2bb6a5-9c42-4189-a9a0-0e499ccb766a",
"sink_id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"source_name": "output",
"sink_name": "discord_bot_token",
"is_static": false
},
{
"id": "ccd08d1f-7ccc-42fa-882c-91f6991ad5e8",
"source_id": "b09e201a-cd71-42d4-a197-22e7eebc54c9",
"sink_id": "164bc3ea-e812-4391-a62d-bdddcf86f3cd",
"source_name": "output",
"sink_name": "input1",
"is_static": false
},
{
"id": "3ed20f9c-3f79-41e4-8fab-0309e92ac629",
"source_id": "60ba4aac-1751-4be7-8745-1bd32191d4a2",
"sink_id": "f3d62f22-d193-4f04-85d2-164200fca4c0",
"source_name": "channel_name",
"sink_name": "channel_name",
"is_static": false
},
{
"id": "89a129e5-11d2-4fac-9a15-7de182a2b806",
"source_id": "164bc3ea-e812-4391-a62d-bdddcf86f3cd",
"sink_id": "b29e3831-3fb7-41bd-88d8-ce3a5dde3d69",
"source_name": "output",
"sink_name": "prompt",
"is_static": false
},
{
"id": "7978ef39-d862-441d-936f-8da60fefcab6",
"source_id": "10759047-6387-4ff1-9117-bbef47d24ee8",
"sink_id": "b09e201a-cd71-42d4-a197-22e7eebc54c9",
"source_name": "output",
"sink_name": "input1",
"is_static": false
},
{
"id": "32e3bace-5df7-4683-97f2-7d9864878aee",
"source_id": "3169d1a8-b541-43f7-97ce-ddc6aecb2080",
"sink_id": "5658c4f7-8e67-4d30-93f2-157bdbd3ef87",
"source_name": "positive",
"sink_name": "query",
"is_static": false
},
{
"id": "0ab7dce1-84b6-4f96-9eb2-1b458fe205a5",
"source_id": "3169d1a8-b541-43f7-97ce-ddc6aecb2080",
"sink_id": "3b2bb6a5-9c42-4189-a9a0-0e499ccb766a",
"source_name": "negative",
"sink_name": "input",
"is_static": false
}
]
}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: autogpt_builder
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "autogpt_builder.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "autogpt_builder.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "autogpt_builder.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "autogpt_builder.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "autogpt_builder.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "autogpt_builder.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "autogpt_builder.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "autogpt_builder.labels" -}}
helm.sh/chart: {{ include "autogpt_builder.chart" . }}
{{ include "autogpt_builder.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "autogpt_builder.selectorLabels" -}}
app.kubernetes.io/name: {{ include "autogpt_builder.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "autogpt_builder.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "autogpt_builder.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "autogpt_builder.fullname" . }}
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "autogpt_builder.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "autogpt_builder.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "autogpt_builder.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "autogpt_builder.fullname" . }}
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "autogpt_builder.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "autogpt_builder.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,7 @@
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: {{ include "autogpt-builder.fullname" . }}-cert
spec:
domains:
- {{ .Values.domain }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "autogpt_builder.fullname" . }}
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "autogpt_builder.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "autogpt_builder.serviceAccountName" . }}
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "autogpt_builder.fullname" . }}-test-connection"
labels:
{{- include "autogpt_builder.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "autogpt_builder.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,77 @@
# dev values, overwrite base values as needed.
image:
repository: us-east1-docker.pkg.dev/agpt-dev/agpt-builder-dev/agpt-builder-dev
pullPolicy: Always
tag: "latest"
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: "dev-agpt-builder-sa@agpt-dev.iam.gserviceaccount.com"
name: "dev-agpt-builder-sa"
service:
type: ClusterIP
port: 8000
targetPort: 3000
annotations:
cloud.google.com/neg: '{"ingress": true}'
ingress:
enabled: true
className: "gce"
annotations:
kubernetes.io/ingress.class: gce
kubernetes.io/ingress.global-static-ip-name: "agpt-dev-agpt-builder-ip"
networking.gke.io/managed-certificates: "autogpt-builder-cert"
kubernetes.io/ingress.allow-http: "true"
hosts:
- host: dev-builder.agpt.co
paths:
- path: /
pathType: Prefix
backend:
service:
name: autogpt-builder
port: 8000
defaultBackend:
service:
name: autogpt-builder
port:
number: 8000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
domain: "dev-builder.agpt.co"
env:
APP_ENV: "dev"
NEXT_PUBLIC_AGPT_SERVER_URL: "http://agpt-server:8000/api"
GOOGLE_CLIENT_ID: "638488734936-ka0bvq73ub3h4cb6013s3lftsl5l04nu.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET: ""
NEXT_PUBLIC_SUPABASE_URL: "https://adfjtextkuilwuhzdjpf.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY: ""

View File

@@ -0,0 +1,76 @@
# base values, environment specific variables should be specified/overwritten in environment values
replicaCount: 1
image:
repository: us-east1-docker.pkg.dev/agpt-dev/agpt-builder-dev/agpt-builder-dev
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
automount: true
annotations: {}
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
securityContext: {}
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetMemoryUtilizationPercentage: 80
volumes: []
volumeMounts: []
nodeSelector: {}
tolerations: []
affinity: {}
domain: ""